Ganska vanliga lösningar på ganska vanliga uppgifter

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!

Göra något med strängar

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");

Göra något med collections / listor

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.

För att skapa en lista används dessa klasser:

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)

Skapa en mappning, som dict i Python

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.

Skapa menyer

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(...);

Göra en layout med flikar

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.

Byta mellan olika komponenter

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.

Fråga användaren något

Om man vill fråga användaren något i ett GUI-program, har Java standardiserat stöd för dialogrutor.

Mer information finns på Java Tutorial om dialogrutor.

Reagera på tangenttryckningar

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.

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");

        final ActionMap act = pane.getActionMap();
        act.put("quit", new QuitAction());
    }

    private class QuitAction extends AbstractAction {
        @Override public void actionPerformed(final ActionEvent e) {
            System.exit(0);
        }
    }
}

Bra att veta:

Stänga ett fönster

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.

Rita ut en bild (en bitmap, t.ex. PNG, JPG)

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!

Inläsning av bild inuti 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).

Identifiera inte filen med ett filnamn. Använd inte klassen 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.

Däremot är det inte OK att fånga upp felet vid inläsningen och krascha senare istället. Detta skulle t.ex. hända om vi hade en felkontroll som gjorde att 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.

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:

Måla med texturer

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...)   

Måla genomskinligt

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.

Måla med kantutjämning

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".

Hitta typsnitt

För nästan alla projekt räcker det säkert med Javas standardtypsnitt:

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();
        ...
    }
}

Upptäcka kollisioner på skärmen

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.

Spela upp ett ljud

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.

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.

Använda filer

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.

Läsa och lagra objekt på fil

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());
	  ...
	
Vi rekommenderar att man inte hårdkodar, utan lagrar banor och liknande i filer istället!

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.

Gson är väl värt att lära sig använda. Det tar bara några rader att konvertera ett objekt, även ett sammansatt objekt som en lista, till en motsvarande textsträng som kan sparas i en fil som är enkel att läsa. På samma sätt kan vi lätt skapa objekt som t.ex. innehåller olika inställningar, spelbanor eller liknande, som sedan kan sparas och läsas med några rader kod.

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.

Serialisering – rekommenderas inte

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.

Serialisering i Java har en del problem som gör att man måste veta exakt vad man håller på med, speciellt under utvecklingen av ett program då det är väldigt lätt hänt att gamla sparade filer blir helt inkompatibla med ny kod. För denna kurs rekommenderar vi starkt att man främst använder sig av andra tekniker, t.ex. Gson som sparar ner objekt i JSON-format (se ovan).

Slut på uppgifter och lösningar!

Föreslå gärna egna, om du tycker det finns information som saknas och som skulle hjälpa till i kursen.