Den här kursen har en central kärna med kunskaper och färdigheter som alla kommer att behöva, och en stor "periferi" där kunskaper och färdigheter bland annat beror på hur man utformar sitt projekt. Sett över flera års tid är det många uppgifter som återkommer gång på gång, och vi vill gärna hjälpa till med dem. Samtidigt har varje enskild grupp bara nytta av en liten del av detta under kursen – och vet inte i förväg vilken del som behövs.
Därför samlar vi lösningar på dessa ganska vanliga uppgifter här, istället för att alla ska delta i ett antal extra föreläsningar där inget är centralt för kursmålen och bara en bråkdel är nödvändigt för projekten. En del av materialet använder sig av tidigare föreläsningsbilder för att illustrera.
(Sedan kan ju en del av det ändå vara intressant för många, men skulle vi ta upp allt som är intressant skulle kursen ta flera år...)
Vi inkluderar även en del lösningar som många har nytta av, men som rör rena fakta snarare än begrepp att förstå, så att det är bättre att slå upp dem när man behöver dem.
Detta kan ändras och uppdateras under kursens gång. Titta gärna tillbaka hit!
Som vi har sagt på en av de första föreläsningarna skiljer Java på tecken och strängar (0 eller flera tecken):
Python har en hel del specialsyntax för strängar som förenklar hanteringen och indexeringen. Java har mindre sådan syntax, och använder istället vanliga metodanrop för att manipulera strängarna. Därför blir stränghanteringen lite mindre koncis.
Om vi vill plocka ut tecken ur strängar, börjar index på 0, precis som i Python:
>>> s = "Spamalot"
>>> s[1]
'p'
>>> s[-2]
'o'
>>> len(s)
8
>>> s[len(s)-1]
't'
String s = "Spamalot";
s.charAt(1)
s.charAt(s.length() – 2)
s.length()
s.charAt(s.length() – 1)
Om vi vill plocka ut delar av strängar:
>>> s = 'Spamalot'
>>> s[1:]
'pamalot'
>>> s[1:3]
'pa'
>>> s[:5]
'Spama'
>>> s[:-1]
'Spamalo'
>>> s[:]
'Spamalot'
>>> " abc ".strip()
'abc'
>>> longstring.splitlines()
String s = "Spamalot";
s.substring(1)
s.substring(1,3)
s.substring(0,5)
s.substring(0, s.length() – 1)
s
s.strip()
s.lines()
Om vi vill jämföra strängar, använder vi inte operatorn "==". Den finns visserligen, men den testar om två variabler pekar på samma strängobjekt (samma minnesadress), medan equals() testar om två strängar innehåller samma tecken, vilket man oftare är intresserad av. Mer om detta på en föreläsning.
>>> "Aardvark" < "Abbot"
True
>>> "Queen" > "King"
True
>>> "a" * 4 == "aaaa"
True
>>> "a" in "The Holy Grail"
True
>>> "vers" in "universitet"
True
>>> "a" == "b"
False
"Aardvark".compareTo("Abbot") < 0
"Queen".compareTo("King") > 0
("a"+"a"+"a"+"a").equals("aaaa")
"The Holy Grail".contains("a")
"universitet".contains("vers")
"a".equals("b")
För att konvertera text till numeriska värden:
val = int("127")
val = float("218.52")
int val = Integer.parseInt("127");
double val = Double.parseDouble("218.52");
För att konkatenera (sätta ihop) strängar används "+". Detta fungerar även om man sätter ihop strängar med tal. I så fall konverteras talen automatiskt till strängar (kompilatorn sätter in extra anrop för konverteringen).
int height = 100;
int width = 200;
String description = "Height is " + height + " and width is " + width;
Man kan också "multiplicera" strängar, som i Python:
"xyz" * 3
"xyz".repeat(3)
För att skriva ut strängar (eller annat) används System.out.println()
:
int height = 100;
int width = 200;
System.out.println("Height is " + height + " and width is " + width);
System.out.println(description);
System.out.println(width);
För att skriva oskrivbara tecken, sådana som inte "syns" på skärmen eller som man inte hittar på tangentbordet, finns ett antal "escapekoder". En del av dessa har ärvts från C.
'\nnn': Tecken med oktalt värde nnn ('\012' == tecken 10 = line feed)
'\unnnn': Tecken med hex-värde nnnn ('\u03D0' == grekiskt beta)
'\b': Backspace (ASCII 8)
'\t': Tab (ASCII 9)
'\n': Newline (ASCII 10)
'\f': Form feed (ASCII 12)
'\r': Carriage return (ASCII 13)
'\'': Apostrof (')
'\\': Backslash (\)
För att konvertera något till en sträng används den statiska hjälpmetoden
String.valueOf()
, som är överlagrad (overloaded) för olika typer.
// Finns redan
public class String {
static String valueOf(char c) { … }
static String valueOf(int i) { … }
static String valueOf(boolean b) { … }
...
}
// I vår egen kod
String str = String.valueOf(127); // "127"
Notera att detta inte behövs när vi konkatenerar strängar, enligt ovan.
Det finns också särskilda metoder för att formatera strängar, t.ex. lägga till inledande nollor:
String str = String.format("%05d", 42) // "00042"
För att konvertera strängar till numeriska värden:
val = int("127")
val = float("218.52")
int val = Integer.parseInt("127");
double val = Double.parseDouble("218.52");
Grunderna i Javas hantering av listor diskuteras under en föreläsning. Här ska vi kortfattat visa mer saker man kan göra med dem. Tanken är främst att sortera ut det som är viktigast inom kursen så att ni inte missar det. Därför hänvisar vi till Javas API-dokumentation för mer detaljer!
Variabeln list står så klart för en lista. Variabeln coll står för en godtycklig
collection, vilket betyder att en lista fungerar men det kan även vara t.ex. en mängd (Set). Många av
metodnamnen antas vara uppenbara. Vissa metoder finns inte i själva listan, utan är statiska hjälpmetoder i
Collections
-klassen.
ArrayList – klart vanligast, implementationen liknar föreläsningsexempel
LinkedList – om man har specifika krav på snabb insättning/borttagning mitt i en lista
Stack – gammal klass, används normalt inte
Vector – gammal klass, används normalt inte
För att lägga till eller ändra element:
coll.add(element)
list.add(int index, E value) – lägg till på given position; flytta resten av elementen
list.set(int index, E value) – ersätt elementet på given position; storlek ändras ej
coll.addAll(coll2) – lägg till element från en annan Collection, t.ex. en lista
list.addAll(int index, Collection c)
Collections.fill(list, element) – ersätter existerande objekt
Collections.replaceAll(list, element1, element2)
För att plocka ut specifika element eller delar:
list.get(index)
list.sublist(fromInclusive, toExclusive) – en del av listan
coll.toArray() – en Object[ ] med samma element
För att testa innehållet / leta efter element:
coll.isEmpty()
coll.size()
coll.contains(Object obj)
coll1.equals(Collection c2) – exakt samma element?
coll1.containsAll(Collection c2) – alla element i coll2 finns i coll1?
list.indexOf(Object needle)
list.lastIndexOf(Object needle)
Collections.binarySearch(list, needle, ...) – binärsökning i sorterad lista
För att ta bort element:
coll.clear() – tar bort alla element
coll.remove(Object)
list.remove(index)
För att ändra listan på andra sätt:
list.sort() – sortera, om elementen är Comparable (se föreläsning)
list.sort(comparator) – sortera, annars
Collections.shuffle(list) – flytta om alla elementen i listan
Collections.rotate(list, distance) – rotera listan (flytta element d positioner)
Python stödjer dicts, en förkortning till dictionaries, som associerar nycklar med värden. Detta kan ses som en generalisering av arrayer eller listor, så att man inte bara kan ha heltal som index utan godtyckliga värden. Att använda detta är överkurs, men vi vill ändå peka er rätt om ni skulle ha användning av den sortens funktionalitet.
Java har så klart en motsvarighet, men precis som för listor finns det ingen specialsyntax för detta. Istället används gränssnittet Map och dess implementationer, och alla operationer sker via metodanrop. Därför blir det som vanligt lite mer att skriva i Java.
>>> dict = {'a': 45, 'b': 39, 'c': 19}
>>> dict['a']
45
>>> dict['d'] = 4711
>>> dict
{'a': 45, 'c': 19, 'b': 39, 'd': 4711}
>>> len(dict)
4
public static void main(String[] args) {
Map<Character,Integer> dict =
new HashMap<>();
dict.put('a', 45);
dict.put('b', 39);
dict.put('c', 19);
System.out.println(dict.get('a'));
// 45
dict.put('d', 4711);
System.out.println(dict);
// {a=45, b=39, c=19, d=4711}
System.out.println(dict.size());
// 4
}
Viktigt: En simpel implementation av Map
skulle visserligen kunna slå upp ett värde
genom att iterera över en lista ända till den hittar något som matchar, men detta skulle vara enormt
ineffektivt. I en Map med en miljon mappningar (nyckel/värde) skulle vi i genomsnitt behöva söka genom en
halv miljon, och testa om det var rätt nyckel, innan vi hittade rätt.
Därför måste nyckelklassen uppfylla vissa krav som Map-klassen kan förlita sig på. String, wrappertyper (Character, Integer med mera), enumtyper och många andra klasser uppfyller redan detta, så vill ni ha dessa som nycklar ska det automatiskt fungera. Om en egen klass ska användas som nyckel (första parametern i put), måste ni däremot tänka lite längre. Detta kommer egentligen i TDDD86, så vi kommer inte att förklara alla detaljer här!
TreeMap förlitar sig på att man kan jämföra objekt med varandra, och se vilket som är "mindre"
respektive "större". Det ställer samma krav som sortering, som kommer att diskuteras på föreläsningarna:
Nyckeltypen ska implementera Comparable
, eller så får man ge TreeMap-objektet en Comparator
.
Då kan TreeMap
lagra informationen i ett röd-svart träd (en struktur som ni får lära er mer om
i senare kurser), och behöver bara titta på ett logaritmiskt antal nycklar för att hitta rätt (kanske i
storleksordningen 20 nycklar vid en miljon mappningar).
HashMap använder sig istället av hashkoder. Detta kan ni läsa mer om på nätet, eller få förklarat i TDDD86.
Med menyer menar vi den här typen av pulldown-menyer:
I Swing (Javas GUI-system) är menyer komponenter precis som knappar och textfält, men de hanteras på ett annat sätt än de flesta komponenter. De "vanliga" komponenterna läggs till med fönstrets egen add()-metod, men då hamnar de i själva verket i en speciell panel som kallas content pane. Ovanför detta finns en särskild plats för en menyrad. Det gör att man inte behöver tänka särskilt på layouthantering för menyerna:
För att lägga till menyer skapar man först en menyrad. Därefter lägger man till menyer i menyraden, och menyval i menyerna. Se följande exempel:
final JMenuBar bar = new JMenuBar();
final JMenu file = new JMenu("File");
file.add(new JMenuItem("Open", 'O')); // Understruket O
file.add(new JMenuItem("Save As", 'A'));
file.addSeparator(); // Separator
file.add(new JMenuItem("Print"));
bar.add(file);
final JMenu edit = new JMenu("Edit");
edit.add(new JMenuItem("Cut"));
edit.add(new JMenuItem("Copy"));
edit.add(new JMenuItem("Paste"));
bar.add(edit);
final JMenu help = new JMenu("Help");
help.add(new JMenuItem("About WordProcessor 1.0"));
bar.add(Box.createHorizontalGlue()); // "Glue" fungerar som en fjäder, trycker "help" till höger
bar.add(help);
frame.setJMenuBar(bar);
Se även Java Tutorial om menyer respektive separatorer.
Händelsehantering för menyer hanteras på samma sätt som för knappar eller andra komponenter: En ActionListener anropas när ett menyval aktiveras, oavsett hur det sker (mus/trackpad, tangentbord, ...).
final JMenuItem open = new JMenuItem("Open");
fileMenu.add(open);
open.addActionListener(...);
Med hjälp av JTabbedPane kan du göra en layout med flikar där användaren själv kan välja flik, enligt nedan. Se även informationen i Java Tutorial.
Om du istället vill att programmet ska byta innehåll i fönstret, utan att användaren kan göra det, går du till nästa avsnitt.
Med hjälp av CardLayout
kan du göra en layout där du lägger in flera komponenter, men där bara en av dem i taget är synlig. Till
skillnad från JTabbedPane
ovan är det inte användaren utan programmet som byter mellan "korten"
i CardLayout
. Detta kan t.ex. användas om du vill byta mellan att fönstret visar en introskärm,
en spelkomponent, en highscorelista, och så vidare – utan att du behöver ta bort den gamla komponenten
och sedan lägga till en ny.
CardLayout
är svår att illustrera eftersom den helt enkelt visar en komponent i taget, utan att
ha några synliga kontroller för att byta komponent. Läs informationen i Java Tutorial för att förstå
hur det fungerar.
Om man vill fråga användaren något i ett GUI-program, har Java standardiserat stöd för dialogrutor.
En enkel fråga med en sträng som resultat:
String input = JOptionPane.showInputDialog("Please input a value");
Man kan också ange ett fönster som äger dialogrutan. Då blockeras all inmatning till fönstret under tiden dialogrutan visas, vilket kan vara bra i vissa fall.
JOptionPane.showMessageDialog(frame, "Eggs aren't supposed to be green.");
Det finns många fler parametrar som kan anges.
JOptionPane.showMessageDialog(frame,
"Eggs are not supposed to be green.",
"Inane warning",
JOptionPane.WARNING_MESSAGE);
Många fler...
// Custom button text
Object[] options = {
"Yes, please",
"No, thanks",
"No eggs, no ham!"
};
int optionChosen = JOptionPane.showOptionDialog(
frameParent, // A window that “owns” the dialog
"Would you like some green eggs to go with that ham?",
"A Silly Question",
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options, // Custom text
options[2] // Default choice
)
Det finns färgväljare:
Och det finns givetvis fildialoger.
Mer information finns på Java Tutorial om dialogrutor.
I Java finns det två huvudsakliga sätt att få något att hända när man trycker (eller släpper) en tangent i ett GUI-program.
Använda en KeyListener
som blir informerad när något händer på tangentbordet, och sedan
själv ta reda på vad det var som hände.
Sätta upp en InputMap
och en ActionMap
som kopplar tangenthändelser till
handlingar.
Det första sättet kan vara ganska knepigt, bland annat för att det alltid är en specifik GUI-komponent som
har tangentbordsfokus
och man måste se till att just den komponenten har en lyssnare. InputMap
plus
ActionMap
ser mer komplicerade ut, men brukar leda till färre problem i praktiken.
Därför går vi genom den metoden, och använder föreläsningens "ordbehandlarexempel" som bas.
I ordbehandlaren kan vi definiera en handling som kan utföras. Detta är en speciell typ av ActionListener
.
Vi kan få hjälp att skapa den genom att ärva från hjälpklassen AbstractAction
, så behöver vi
bara implementera själva actionPerformed
-metoden precis som "vanligt".
public class WordProcessor20 {
...
private class QuitAction extends AbstractAction {
@Override public void actionPerformed(final ActionEvent e) {
// Gör vad som behövs -- öppna dialogruta och fråga, ...
System.exit(0);
}
}
}
Nu vill vi se till att en QuitAction
anropas när någon trycker Alt-F4. Tangentkombinationen
kallas en KeyStroke
, och med en InputMap
kopplas den till ett namn på en
handling. Sedan används en ActionMap
för att koppla namnet till koden, det vill säga objektet
av typen QuitAction
.
Den kopplingen gör vi så här (förklaringar efter):
public class WordProcessor20 {
private JFrame frame;
public WordProcessor20() {
this.frame = new JFrame("Word Processor 1.0");
JComponent pane = frame.getRootPane();
final InputMap in = pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
in.put(KeyStroke.getKeyStroke("Alt F4"), "quit");
in.put(KeyStroke.getKeyStroke("Ctrl Q"), "quit");
in.put(KeyStroke.getKeyStroke("Alt F5"), "fail");
// Inte när vi trycker ner Space, men när vi till slut släpper tangenten
in.put(KeyStroke.getKeyStroke("released SPACE"), "fail");
final ActionMap act = pane.getActionMap();
act.put("quit", new QuitAction(0));
act.put("fail", new QuitAction(1));
}
private class QuitAction extends AbstractAction {
private final int exitCode;
private QuitAction(int exitCode) {
this.exitCode = exitCode;
}
@Override public void actionPerformed(final ActionEvent e) {
// Exit code 0 == success
// Other exit codes signal failure
System.exit(exitCode);
}
}
}
Bra att veta:
Ovan skapar vi en QuitAction
-klass som tar en parameter som sparas undan. Det är en bra
generalisering som låter oss använda samma klass för både "quit" och "fail", istället för att
behöva skapa nya klasser för varje "variant" av avslutningen. Klasser är tungviktiga och vi vill
hellre bara skicka med olika data till samma klass än att skapa nya klasser i onödan. Samma gäller
vid move-handlingar där man hellre skickar med en riktningsparameter än att skapa en ny klass för varje
riktning!
Varje komponent har sina egna InputMaps. I exemplet tar vi en InputMap hos RootPane
, som
innehåller "allt annat" som finns i ett fönster – jatt förstå exakt vad detta betyder är överkurs,
men den intresserade kan läsa mer i Java Tutorial.
När vi hämtar InputMap anger vi JComponent.WHEN_IN_FOCUSED_WINDOW
. Detta betyder att vi är
intresserade av tangenttryckningar när komponentens fönster har fokus (är "valt" på skärmen).
Väljer vi istället JComponent.WHEN_FOCUSED
måste komponenten själv ha fokus. Det är ju
användbart t.ex. i ett textfält: Då vill vi inte att "pil vänster" flyttar markören om det är ett
annat textfält som har fokus! Men i den här kursen används tangenttryckningar oftast i spel
där man vill ha en mer "global" koppling, så man slipper bry sig om var tangentbordsfokus ligger.
Sedan anger vi att Alt-F4 ska kopplas till strängen "quit". Se KeyStroke och Key Bindings för information om vilka KeyStrokes som kan anges. Exempel är "LEFT", "released SPACE", "control DELETE", "alt shift released X", och "typed a".
Det går alltså bra att säga att något ska hända när en tangent trycks ner ("pressed SPACE") eller när den släpps upp ("released X") eller liknande varianter.Till slut kopplar vi strängen "quit" till en handling av typen QuitAction
.
För kursens skull behöver vi oftast inte stänga fönster: Det går ju bra att stoppa ett helt program från utvecklingsmiljön. Ändå kanske man vill ha den möjligheten i en del projekt.
För att ifrån koden stänga fönstret frame
anropar man frame.dispose()
.
För att bestämma vad stäng-knappen gör använder man (så snart fönstret har skapats) frame.setDefaultCloseOperation(x)
,
där x är...
Ibland kanske man vill bestämma själv vad stäng-knappen ska göra, t.ex. öppna en dialogruta för att fråga om
användaren verkligen är säker. Då använder man WindowConstants.DO_NOTHING_ON_CLOSE
och lägger
sedan till en WindowListener
som blir informerad när någon trycker på knappen. WindowListener
ser ut såhär:
package java.awt.event;
public interface WindowListener extends EventListener {
public void windowOpened(WindowEvent e);
public void windowClosing(WindowEvent e); // När stängknappen trycks!
public void windowClosed(WindowEvent e);
public void windowIconified(WindowEvent e);
public void windowDeiconified(WindowEvent e);
public void windowActivated(WindowEvent e);
public void windowDeactivated(WindowEvent e);
}
Man får alltså skapa en lyssnare som implementerar detta gränssnitt och som ger de flesta metoderna, som man
inte är intresserad av, helt tomma implementationer. Alternativt kan man låta lyssnaren ärva från klassen
WindowAdapter
, som är en trivial hjälpklass som helt enkelt har tomma implementationer av alla
metoder från WindowListener
:
package java.awt.event;
public class WindowAdapter implements WindowListener {
public void windowOpened(WindowEvent e) { }
public void windowClosing(WindowEvent e) { }
public void windowClosed(WindowEvent e) { }
public void windowIconified(WindowEvent e) { }
public void windowDeiconified(WindowEvent e) { }
public void windowActivated(WindowEvent e) { }
public void windowDeactivated(WindowEvent e) { }
}
I så fall kan resultatet se ut så här:
public class AskBeforeClosing {
private JFrame frame;
public AskBeforeClosing() {
this.frame = new JFrame("Try to close me");
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new CloseButtonHandler());
}
private class CloseButtonHandler extends WindowAdapter {
public void windowClosing(final WindowEvent e) {
int answer = JOptionPane.showConfirmDialog
(frame, "Do you really want to quit?", "Quit?", JOptionPane.YES_NO_OPTION);
if (answer == JOptionPane.YES_OPTION) {
System.exit(0); // Avsluta hela Java-processen
}
}
}
}
Se även info om dialogrutor.
Java har flera olika sätt att hantera bitmapbilder (JPG, PNG med mera), beroende på hur avancerad funktionalitet man behöver.
När Java introducerades, i mitten av 90-talet, fanns klassen java.awt.Image
. Det finns numera
också varianter som BufferedImage
som låter oss manipulera bilder i minnet. Men
Image
anpassades till användning i applets, Java-program som kör på websidor, och till dåtidens
långsamma uppkopplingar. Därför kan den läsa in en bildfil från nätet, och den läser alltid
asynkront och inkrementellt så man kan rita ut den del som har kommit fram hittills. Det
gör också att de här klasserna är lite komplicerade att använda.
Istället fokuserar vi på javax.swing.ImageIcon
,
som implementerar Icon
-gränssnittet. Den är enklare att hantera och klarar allt man behöver för
typiska kursprojekt. En ImageIcon kan läsas in från minnet, från disk eller via HTTP när man anger en URL:
new ImageIcon(byte[] data)
new ImageIcon(String filename)
new ImageIcon(URL location)
Användbara metoder inkluderar:
int getIconHeight()
int getIconWidth()
void paintIcon(Component c, Graphics g, int x, int y)
Vi börjar med ett enkelt exempel. Läs varningarna nedan.
public class IconPainter01 extends JComponent {
final ImageIcon icon = new ImageIcon(ClassLoader.getSystemResource("/javatut.jpg"));
public void paintComponent(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
icon.paintIcon(this, g, 50, 50);
}
public static void main(String[] args) {
final JFrame frame = new JFrame("Graphics2D Test");
frame.setLayout(new GridLayout(1,1));
frame.add(new IconPainter());
frame.setSize(300, 300);
frame.setVisible(true);
}
}
Nedan läses en bild in en gång för alla, och en pekare lagras i ett fält i klassen. Vi vill ju inte läsa in bilden varje gång den ska ritas ut!
paintComponent()
blir väldigt ineffektivt, eftersom bilden då läses
varje gång komponenten ritas ut. Utritning ska vara så snabb som möjligt! Lägg bilden i ett fält,
statiskt eller icke-statiskt, och använd den sedan många gånger.
Notera att vi använder ClassLoader.getSystemResource()
för att hitta filen. Detta fungerar
tillsammans med Javas stöd för resurser, som i princip är datafiler som distribueras tillsammans
med ett program. På det sättet undviker vi att behöva ange var filerna finns i en specifik installation, och
filerna kan hämtas även om hela programmet råkar vara packat i en JAR-fil (liknande ZIP-fil).
File
, metoden
url.getFile()
, eller liknande. Använd resurser, som har diskuterats i slutet av
GUI-föreläsningen.
Koden ovan kommer att krascha omedelbart vid inläsningen om den önskade resursen (bildfilen) inte
finns, eftersom getSystemResource()
då returnerar null
, vilket konstruktorn till
ImageIcon
inte gillar. Normalt är detta helt fel, men just när en resurs saknas är det
faktiskt OK: Resursen ses som en fundamental del av programmet, och om en del av programmet saknas är det
fel på installationen. Då kan vi lika gärna sakna en klass som behövs för programmet, och man kan inte
gardera sig mot allt sådant.
icon
blev null
om bilden inte fanns. Då skulle varje anrop till paintComponent()
krascha när man försökte
anropa paintIcon()
-- inte OK!
I paintComponent()
väljer vi att slå på antialiasing och sedan rita
ut bilden på position (50,50). Resten av koden, i main(), öppnar helt enkelt ett fönster och lägger in en
IconPainter01-komponent där.
Vad blir resultatet då?
Oj, bilden fick inte plats! Det kan ju vara bra att kunna ändra storlek på den när den ritas ut, men det
fanns inget storleksargument i paintIcon()
. Istället använder Java en generell
transform, något som ändrar på koordinaterna för allt som ritas ut. Exempel på att skala
allt med en faktor 0.5 i både x-led och y-led:
public class IconPainter02 extends JComponent {
final ImageIcon icon = new ImageIcon(ClassLoader.getSystemResource("/javatut.jpg"));
public void paintComponent(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
final AffineTransform old = g2d.getTransform();
final AffineTransform at = AffineTransform.getScaleInstance(0.5, 0.5);
g2d.transform(at);
icon.paintIcon(this, g, 50, 50);
g2d.setTransform(old);
}
}
Notera ovan att vi sparar undan den gamla transformen för att återställa den efter vi ritar! Notera också
att vi anropar transform()
första gången, för att behålla den transform som komponenten
eventuellt redan har (kanske för att ta hänsyn till fönsterramarnas storlek) och addera vår egen
till den. När vi återställer använder vi setTransform()
.
Originalet till vänster, resultatet till höger:
Ja, transformen skalar verkligen allt -- även koordinaterna (50,50) för övre högra hörnet! Här kan man istället välja att transformera i flera steg. Först kod, sedan förklaring.
public class IconPainter02 extends JComponent {
final ImageIcon icon = new ImageIcon(ClassLoader.getSystemResource("/javatut.jpg"));
public void paintComponent(final Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
final AffineTransform old = g2d.getTransform();
// Steg 4: Gör ingenting (skala faktor 1)
final AffineTransform at = AffineTransform.getScaleInstance(1,1);
// Steg 3: Flytta (i "oskalade" koordinater)
at.translate(50,50);
// Steg 2: Skala ner alla koordinater
at.scale(0.5, 0.5);
g2d.transform(at);
// Steg 1: Starta på (0,0)
icon.paintIcon(this, g, 0, 0);
g2d.setTransform(old);
}
}
Detta ser ju väldigt skumt ut. Man måste i princip läsa det baklänges för att förstå. Anta att bildens storlek är 300x400 pixlar.
Vi anger till paintIcon() att vi vill rita ut på virtuella koordinaterna (0,0), istället för (50,50) som vi tidigare använde. Bilden ska då täcka de virtuella punkterna (0,0) till och med (299,399).
Det sista vi la till i den nuvarande transformen är en skalning med en faktor 0.5. Allt skalas ner, så bilden täcker punkterna (0,0) till och med (149.5, 199.5).
Innan detta la vi till en förflyttning på (50,50) punkter, för att få bildens övre vänstra hörn att hamna rätt. Det läggs till överallt, så bilden täcker punkterna (50,50) till (199.5, 249.5).
Det första vi gjorde var att hämta ut en transform som inte gör något alls -- bara för att få en enkel startpunkt. Bilden täcker fortfarande punkterna (50,50) till (199.5, 249.5).
Nu är vi klara, så bilden ritas ut på de angivna punkterna. Bilden är nu placerad på (50,50) och är hälften så stor som originalet, som vi ville.
Originalet till vänster, resultatet till höger:
Transformer kan också göra annat än att flytta och skala bilder. Till exempel kan vi rotera dem. Då får vi tänka på samma sätt i fråga om vad man gör först. Exempel:
...
final AffineTransform old = g2d.getTransform();
final AffineTransform at = AffineTransform.getScaleInstance(1,1);
at.translate(50,50);
at.scale(0.5, 0.5);
at.rotate(Math.PI/4);
g2d.transform(at);
icon.paintIcon(this, g, 0, 0);
g2d.setTransform(old);
...
För att förstå vad som händer "fuskar" vi genom att visa allting steg för steg i ett större "fuskfönster" där man även ser det som egentligen faller utanför, där koordinaterna (0,0) finns i skärningspunkten mellan de röda linjerna. Rotationen står sist men sker först. Sedan skalas alla koordinater, och till slut förflyttas resultatet.
Originalet till vänster, det verkliga resultatet till höger:
En textur fungerar som den rutiga färgen i Kalle Anka på julafton (om nu någon fortfarande ser på det). Man anger en bild som ska användas, och den repeteras så många gånger som behövs för att fylla ytan med färg. Ytan kan ha godtycklig form. Exempel:
TexturePaint paint = new TexturePaint(smiley,
new Rectangle2D.Double(0, 0, smiley.getWidth(), smiley.getHeight()));
g2.setPaint(paint);
g2.fill(...polygon...)
Ibland kanske man vill måla halvgenomskinligt – till exempel för att en bakgrundsbild delvis ska synas bakom statusinformation:
Detta görs på följande sätt:
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7));
Förenklat kan man säga att vi just nu målar med 30% genomskinlighet (1.0 - 0.7), men den här funktionaliteten är egentligen mycket mer generell: Den talar om precis hur man ska kombinera ett existerande objekt med ett nytt när det målas ut. Mer information finns i dokumentationen för AlphaComposite respektive det ännu mer generella gränssnittet Composite.
Antialiasing (kantutjämning) handlar om olika metoder att minska effekten av en relativt låg upplösning, för att få kanterna på linjer och kurvor att se jämnare ut. Ett exempel är att använda gråskalor istället för bara svart och vitt när text eller linjer ritas ut:
I Java kan man slå på antialiasing separat för text och för andra former än text. Detta görs med hjälp av så kallade Rendering Hints.
g2.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Andra rendering hints kan användas för att påverka andra delar av "utritningen".
För nästan alla projekt räcker det säkert med Javas standardtypsnitt:
Dialog, DialogInput för dialoger
Serif (kan motsvara Times eller annan font med seriffer)
SansSerif (kan motsvara Arial, Helvetica eller annan font utan seriffer)
Monospaced (skrivmaskinsstil)
Symbol (symboler, används sällan)
Men i vissa fall kan man vilja använda andra typsnitt, och dessutom ta reda på vilka typsnitt som finns. Det gör man så här:
public class MyClass {
public void checkFonts() {
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
String[] names = ge.getAvailableFontFamilyNames();
...
}
}
Om man skriver ett spel kan det hända att man behöver testa
om två objekt kolliderar på skärmen. Ett av många sätt bygger på Javas geometriska Shape
-klasser,
som kan representera ungefärlig eller exakt form för objekt på skärmen. Man kan till exempel skapa en bounding
box som omsluter ett objekt:
Denna kan skapas med hjälp av Area-klassen, som låter oss kombinera flera former till en ny form, t.ex. en cirkel och en rektangel. Sedan kan man kontrollera om två shapes, som motsvarar stationära eller rörliga objekt och som har fått koordinater motsvarande var de är på skärmen, kolliderar. Detta fungerar även om man inte använder shapes för att rita objekten.
Shape s1 = ...;
Rectangle2D s2 = new Rectangle2D.Double(...);
if (s1.intersects(s2)) {
// Collision!
}
Utöver detta finns många klassbibliotek som kan underlätta spelprogrammering, men vi har inget specifikt sådant bibliotek att rekommendera.
Java har ett relativt avancerat ljudhanteringssystem med stöd för uppspelning, sampling, ljudbehandling, mixning av ljudkanaler, med mera. Detta kommer vi inte att ta upp i någon större detalj i den här kursen, inte minst för att det är objektorientering och inte ljudhantering som är fokus. Det vi vill visa är istället den enklaste möjliga ljudhanteringen, där man kan starta ett ljud eller loopa det ett visst antal gånger.
Ett ljudklipp (en ljudfil) representeras av klassen Clip
Detta klipp måste läsas in innan det kan användas. Lämpligen görs det en gång för alla, inte varje gång det ska spelas upp!
Vid inläsning använder man lämpligen resurser enligt nedan, så att ljudfilen hämtas från den plats där programmet är installerat.
Självklart måste inte ljud hanteras i en separat klass (SoundPlayer2
), utan detta görs
bara för att få ett litet men relativt fullständigt exempel.
public class SoundPlayer2 {
private final Clip beepSound;
public SoundPlayer2() throws ... {
URL url = SoundPlayer2.class.getResource("/sounds/beep-01a.mp3");
if (url == null) { /* Felhantering eller kasta ett eget exception! */ }
beepSound = AudioSystem.getClip();
beepSound.open(AudioSystem.getAudioInputStream(url));
}
public void beepOnce() { beepSound.start(); }
public void beepTwice() { beepSound.loop(1); }
}
Den intresserade kan se mer på Java
Tutorial och efterföljande sidor.
Filhantering finns inte med på föreläsningarna, men i kursböcker (t.ex. Java Tutorial) och i gamla föreläsningsbilder.
Utöver detta vill vi också nämna klassen java.nio.file.Files
,
som innehåller en hel del smått och gott i fråga om filhantering. Bland annat finns
readString()
för att läsa in en hel fil som en sträng, readAllLines()
för att få
den som en lista av rader, och readAllBytes()
för att få den som en byte-array.
För att hantera sökvägar och filnamn används lämpligen java.io.file.Path
.
Sådana konstrueras med Path.of(...)
(se API-sidan). Den gamla java.io.File
är till
stor del föråldrad.
Mer funktionalitet kan hittas på paketsidorna för java.nio.file.
I många projekt används fördefinierade data av olika slag – inte bara bilder och ljud, utan banor, startpositioner för spelbräden, och många andra sorters information.
När det gäller bilder finns det ju färdiga lagringsformat, som .PNG-filer, och färdig kod som läser in dem.
Där blir det enklast att lagra dem i filer och läsa in dem med hjälp av klasser som redan finns,
t.ex. ImageIcon
.
När det gäller andra data kan det istället vara enklast att göra fel – i alla fall i början. Vill man bygga upp olika banor kan man t.ex. hårdkoda dem:
world.place(10, 20, new Rock()); world.place(11, 20, new Water()); ...
Här rekommenderar vi att man använder sig av Gson för att lagra informationen i en fil. Då skiljer vi på data (som lätt kan ändras i en fil utan omkompilering) och kod (som definierar beteenden och som kan exekveras). Gson gör bland annat att vi inte själva behöver skriva koden som läser ett visst filformat, och beskrivs närmare i uppgiften "Spara på fil" i Tetris. Det format som används är JSON, JavaScript Object Notation.
För att läsa och skriva sådana strängar: Använd t.ex. readString() och writeString(), som diskuterades ovan, för att läsa hela den sträng som Gson skapade eller vill ha.
Utöver bibliotek som Gson (ovan) har Java också alltid haft inbyggd funktionalitet för att skriva ner en representation av ett helt objekt till en fil (eller någon annan destination, till exempel en databas). Alla objekt det i sin tur pekar på skrivs också ner, eftersom objektet annars inte skulle "peka rätt" när det lästes tillbaka. Skriver man en lista till en fil skickar Java alltså även med objekten som listan innehåller, precis som man skulle vänta sig.
Detta kallas serialisering, eftersom man har ett antal objekt som ligger placerade på olika platser i minnet och pekar på varandra, och man vill spara tillståndet i alla dessa objekt som en serie av bytes som kan sparas undan någonstans. Detta kan också kallas marshalling (generellt), eller pickling i Python (sylta / marinera). Serialisering finns i många språk, även sådana som inte är objektorienterade: Ett generellt sätt att serialisera data är användbart för alla sorters sammansatta datatyper.
Föreslå gärna egna, om du tycker det finns information som saknas och som skulle hjälpa till i kursen.