Detta dokument förutsätter Netscape 4 eller Internet Explorer 4, om du har tidigare versioner klicka här.

Innehåll, föreläsning 4 (Java AWT och händelsehantering)

Föreläsning 4: Java AWT och händelsehantering

En nästan fullständig objektmodell av de klasser - Abstract Windowing Toolkit (AWT) - som utgör Javas byggstenar för att bygga grafiska användargränssnitt (s.k. GUI - Graphical User Interface) ser ut så här:

(OBS! I Java JDK 1.2 finns en ny uppsättning GUI-klasser (Swing) som på sikt troligen kommer att ersätta AWT. Principerna behålls i denna standard som mest ändrar utseende och namn på existerande komponenter och lägger till en mängd nya. Men plugga alltså inte in detaljerna i AWT eftersom de kommer att förlora i betydelse).

Dessa klasser kan sedan delas in i olika grupper:

Övriga klasser kan ses som huvudsakligen abstrakta eller sådana som används internt av andra klasser.

Ett användargränssnitt består av en behållare (Frame i applikationer, Panel i applets - Applet är en subklass till Panel) som andra komponenter placeras i. Eftersom dessa komponenter i sin tur kan vara behållare (Panels) kan man bygga hur komplicerade gränssnitt som helst.

Exakt hur de olika komponenterna placeras i sin behållare bestäms av en s.k. layout-hanterare: klasserna som har namn som slutar på ...Layout. Man placerar alltså normalt inte komponenterna på fasta positioner (även om detta också är möjligt) eftersom man vill att utformningen skall vara så oberoende som möjligt av fönstersystem. Varje behållare har en layout-hanterare kopplad till sig. Alla layout-hanterarna implementerar gränssnittet LayoutManager. Metoden setLayout(LayoutManager) i Container används för att koppla en layout-hanterare till en behållare - om man skulle vilja placera ut komponenterna själv skall man anropa setLayout med argumentet null.

Behållare och komponenter

Alla komponenter utom fönster (Window och Dialog) måste placeras i en behållare för att synas. Man placeras en komponent genom att anropa någon av behållarens add-metoder. Vilken metod man bör använda styrs av vilken layout-hanterare man valt.

Klassen Component har metoder för

Varje komponent har sitt eget koordinatsystem där x ökar till höger och y nedåt. Notera att positionen normalt inte anges på detta sätt, man överlåter ansvaret på en layout-hanterare. Positionen anges i behållarens koordinatsystem.

Klicka här för en utförligare beskrivning av AWT.

Händelsehantering

OBS! detta avsnitt beskriver händelsehanteringsmodellen som finns i JDK 1.1 som skiljer sig en del från den modell som JDK 1.0 hade.

Interaktivt styrda program kan inte skrivas enligt det traditionella sekventiella mönstret: initiera, kör algoritmen, avsluta. I stället använder man s.k. händelsestyrd programmering. Händelser är sådant som användaren gör, t.ex. klickar med musen, tar bort ett fönster. I AWT genererar alla sådana händelser ett händelseobjekt som sedan tas om hand av en s.k. lyssnare (Listener). Klasser som har med händelser att göra finns i paketet java.awt.event (AWTEvent ligger i java.awt). De viktigaste händelserna:

Som Java-programmerare behöver man oftast inte hantera händelseobjekten - det räcker att veta att en händelse inträffat. Några undantg:

Det andra viktiga begreppet för handelsehantering är lyssnarna: de är gränssnitt, inte klasser. Klasser som behöver få reda på att händelser inträffar implementerar således en lyssnare. En händelsehanterare kan vara en instans av vilken klass som helst. Tre steg kan urskiljas i hanteringen av händelser.
  1. Man skriver en klass, en händelsehanterare, och talar om att klassen implementerar ett lyssnargränssnitt:
    public class MyEventHandler implements ActionListener {
    ...
    }
    
    Man kan låta händelsehanteraren vara en egen klass som här eller man kan implementera lyssnaren i någon annan klass.

  2. En instans av klassen MyEventHandler måste skapas och den komponent vars händelser man är intresserad av måste informeras om lyssnaren:
    MyEventHandler oneEventHandler = new MyEventHandler();
    Button b = new Button();
    b.addActionListener( oneEventHandler );
    
  3. Kod för att ta hand om händelsen måste skrivas i MyEventHandler:
    public void actionPerformed( ActionEvent event )
    {
     ... gör vad som ska göras
    }
    
Observera att händelsehanteraren och komponenten (knappen) är helt oberoende av varandra, allt man gör är att man talar om för knappen att man vill veta när den trycks in.

I klassen Component finns metoder för att hantera (lägga till och ta bort) lyssnare:

Olika subklasser lägger till sina egna lyssnare: För fullständighetens skull följer här en uppräkning av några av de viktigaste metoderna i de olika lyssnarna och när de anropas.

ActionListener

actionPerformed(ActionEvent) - en aktion (knapptryck, menyval) inträffar

FocusListener

focusGained(FocusEvent) - komponenten får fokus
focusLost(FocusEvent) - komponenten förlorar fokus

ItemListener

itemStateChanged(ItemEvent) - status har ändrats (från vald till ej vald ellervice versa)

KeyListener

keyPressed(KeyEvent), keyReleased(KeyEvent) - en tangent trycks ned eller släpps.
keyTyped - en tecken har skrivits

MouseListener

mouseClicked(MouseEvent), mousePressed(MouseEvent), mouseReleased(MouseEvent)
mouseExited(MouseEvent), mouseEntered(MouseEvent) - muspekaren kommer in i eller lämnar en komponent

TextListener

textValueChanged - texten har ändrats

WindowListener

windowActivated(WindowEvent), windowClosed(WindowEvent), windowClosing(WindowEvent), windowDeactivated(WindowEvent), windowDeiconified(WindowEvent), windowIconified(WindowEvent), windowOpened(WindowEvent)

Eftersom en lyssnare är ett gränssnitt måste man implementera alla metoder även om man bara är intresserad av en av dem. Vi skall snart se att det finns klasser, s.k. adaptorer, som förenklar detta åt oss.

Layout-hanterare

Exakt hur komponenterna i ett användargränssnitt läggs ut bestäms av en s.k. layout-hanterare. Vilken man väljer beror på vad man vill uppnå. Tänk också på att att AWT möjliggör hierarkiska GUIs: man kan t.ex. ha ett fönster med FlowLayout där alla eller några av komponenterna är Paneler, var och en med sin egen layout-hanterare. Alla behållare har en metod setLayout( LayoutManager ) som används för att koppla en hanterare till behållaren (LayoutManager är ett interface som implementeras av layout-hanterarna).

Exemplet efter de korta beskrivningarna visar hur man använder layout-hanterarna).

FlowLayout

FlowLayout (default för Panel) placerar komponenterna i en rad efter varandra eller i flera rader om utrymmet tar slut. Vid konstruktionen av FlowLayout- objektet kan man välja om komponenterna skall centreras, vänsterjusteras eller högerjusteras i det tillgängliga utrymmet.

GridLayout

GridLayout placerar komponenterna i ett rutnät där alla rutor är lika stora. Man kan specifiera antalet rader och kolumner samt avståndet mellan rutorna.

BorderLayout

BorderLayout (default för Frame) delar in behållaren i fem områden: öster, väster, norr, söder och mitten. I varje område kan en komponent placeras. Mittenområdet växer mest om behällaren ändrar storlek.

CardLayout

CardLayout är annorlunda än de övriga på så sätt att endast en komponent syns samtidigt. Komponenterna ses som kort i en kortlek.

GridBagLayout

GridBagLayout är en generalisering av GridLayout där de olika komponenterna placeras i rutnät med olikstora rutor: man använder instanser av GridBagConstraints för att specificera hur. Denna klass är inte speciellt lättanvänd eller intuitiv, men eftersom den existerar nämns den in alla fall här.

Exempel

Denna applet använder GridLayout med 2x2 rutor med en Panel i varje ruta. Dessa i sin tur använder olika typer av layout-hanterare.

Observera att endast tryckningar på knappar i Card-Panelen tas om hand av programmet.

Koden och litet mer information om de olika hanterarna finns här.

Att rita grafik

När en applikation eller applet ska ritas upp - t.ex. när fönstret första gången visar sig på skärmen eller blir synligt efter att ha varit skymt - börjar systemet i huvudfönstret och letar sig rekursivt nedåt i hierarkin. Alla komponenter ritas upp under resans gång.

Program får rita endast på order från systemet; annars kunde konstigheter som att man ritar i icke-existerande fönster inträffa. Ritning fungerar därför så att en komponent begär att få bli ritad genom att anropa metoden repaint(). AWT begär sedan i sin tur att komponenten ska rita sig genom att anropa komponentens update()-metod. Denna metod i sin tur rensar sin rityta och anropar paint().

En fix komponent (som Button) implementerar alltså hela sin grafik i paint()-metoden. Detta gäller de flesta komponenter men om någonting ändras i bilden kan alltså komponenten själv anropa repaint() för att få möjlighet att rita om sig. I animeringar ersätter man också ofta update() med en egen variant. De två metoderna paint() och update() har ett argument: en instans av ett objekt av klassen Graphics som kan ses som den ritmiljö man ritar i. Klassen har metoder för att rita linjer, cirklar, ellipser, skriva text, m.m. Dessutom finns information om hur stor del av fönstret som behöver ritas om.

Grafik kan man rita i flera av AWT-komponenterna men oftast ritar man i instanser av Canvas eller Applet eller (oftare ändå) subklasser till dessa. Det enklaste sättet att rita grafik är att helt enkelt skriva ritinstruktionerna i paint().

Appleten som följer ritar de figurer som klassen Graphics definierar. Det finns också metoder för att rita bilder.

Koden finns här.

Färger

Instanser av klassen java.awt.Color definierar vilken färg man ritar med. Klassen innehåller dels några konstanter för de vanligaste färgerna, dels också några metoder för att manipulera en färg: brighter(), darker().

Man kan också skapa egna färger med konstruktorn:

Color( int redvalue, int greenvalue, int bluevalue);
//Exempel, skapa en gråröd färg:
Color mycolor = new Color( 200, 100, 100);
//Exempel, använd röd färg:
g.setColor( Color.red ); // Använd den fördefinierade konstanten
där färgvärdena skall ligga i intervallet 0 till 255 där 255 har högst intensitet.

Typsnitt

Klassen java.awt.Font inkapslar typsnittshantering i Java-grafik. Dess enda konstruktor är:
Font(String name, int style,int size)
// Exempel:
Font myfont = new Font( "Times", Font.BOLD, 12 );
style skall vara en av Font.BOLD, Font.ITALIC eller Font.PLAIN. Den första parametern anger typsnittsfamilj i form av en sträng: vilka typsnitt som finns är plattformsberoende.

Adaptorer, inre och anonyma klasser

Lyssnargränssnitten kräver att alla metoder implementeras, oavsett hur många man egentligen använder. Om man använder bara någon enstaka metod måste en mängd tomma metoder skrivas, vilket inte bara är onödigt utan också leder till svårlästa program.

För att minska skrivarbetet finns några s.k. adaptorer, tomma implementationer av lyssnargränssitten, definierade. I stället för att implementera ett gränssnitt kan man skapa en subklass till en adaptor:

import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class Scribble extends Applet
{
    private int x;
    private int y;
    MyMouseAdapter mma;
    MyMouseMotionAdapter mmma;
    
    private Color stringToColor ( String colstring )
    {
	if (colstring.equals("red")) return Color.red;
	else if (colstring.equals("green")) return Color.green;
	else if (colstring.equals("blue") )return Color.blue;
	else if (colstring.equals("yellow")) return Color.yellow;
	else return Color.white;
    }

    public void init()
    {
       	setBackground( stringToColor( getParameter("colour")) );
	mma = new MyMouseAdapter();
	mmma = new MyMouseMotionAdapter();
	addMouseListener( mma );
	addMouseMotionListener( mmma );
    }

    public class MyMouseAdapter extends MouseAdapter 
    {
	public void mousePressed( MouseEvent e )
	{ x = e.getX(); y = e.getY(); }	
    }

    public class MyMouseMotionAdapter extends MouseMotionAdapter
    {
	public void mouseDragged( MouseEvent e )
	{
	    Graphics g = getGraphics();
	    g.drawLine( x, y, e.getX(), e.getY() );
	    x = e.getX(); y = e.getY();
	}
    }

}


Flera av de exempel vi sett har nästlade eller inre klasser, d.v.s.klasser som helt definieras inne i andra klasser. Ett exempel är de två adaptorerna i Scribble. Eftersom en Javaklass inte kan ärva mer än en klass kan man inte ärva både Applet och en adaptor utan man får ta till en inre klass.

En inre klass är ett specialfall av en nästlad klass, nämligen en icke statisk nästlad klass. Inre klasser kan komma åt den omgivande klassens metoder och attribut. En instans av en inre klass kan finnas och fungera bara inuti en instans av sin omgivande klass.

Anonyma klasser

Klasserna MyMouseAdapter och MyMouseMotionAdapter i exemplet Scribble har som enda uppgift att implementera händelsehanteringen. Själva namnen är ointressanta och faktum är att Java tillåter icke namngivna inre klasser, s.k. anonyma klasser. Vi skulle kunna förenkla Scribble:
import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class ScribbleAnonymous extends Applet
{
    private int x;
    private int y;
    
    public void init()
    {
       	setBackground( Color.yellow );
	addMouseListener( 
			 new MouseAdapter() 
			 {
			     public void  mousePressed( MouseEvent e )
				 { x = e.getX(); y = e.getY(); }	
			 }
			 );
	addMouseMotionListener(
			       new MouseMotionAdapter()
			       {
				   public void mouseDragged( MouseEvent e )
				       {
					   Graphics g = getGraphics();
					   g.drawLine(x,y,e.getX(),e.getY());
					   x = e.getX(); y = e.getY();
				       } 
			       }
			       );
    }
    
}
Ett överdrivet användande av inre och speciellt anonyma klasser gör koden klart oläsbar varför man bör vara sparsam med dem: anonyma klasser bör ha en eller högst två metoder och vara av typen "klass som gör en enda sak" (som adaptorerna).