Swing

Sven-Olof Nyström
OOP med Java våren -25
Informationsteknologi
Uppsala Universitet

Skansholm: Kapitel 6-8.

Se även Oracles tutorial. (Notera: vi kommer inte att titta på JavaFX.)

Grafiska användargränssnitt (GUI)

Vad är ett GUI?

Det känns lite som en kuggfråga...

Ett grafiskt användargränssnitt är ett gränssnitt till ett program som använder text, bilder och grafiska element för att kommunicera med användaren. Användaren ger input till programmet dels med tangentbordet, dels med hjälp av ett pekdon, tex mus, pekplatta, styrkula eller pekskärm.

Om du läser detta på en datorskärm så tittar du just nu på ett GUI. Moderna OS har grafiska användargränssnitt, och när man startar en applikation har (vanligtvis) även applikationen ett GUI.

Normalt är GUI'n så självklara att man inte tänker på dem, men någon måste programmera dem. I det här avsnittet kommer vi att titta på hur de implementeras. Vi kommer att arbeta med Swing, ett bibliotek för programmering av grafiska användargränssnitt. Mitt mål är att ge allmänna insikter om programmering av GUI som även kan tillämpas i andra sammanhang.

Swing ger också bra exempel på objektorienterad design och användning av Java. Vi kommer även att se några exempel på avancerade konstruktioner i Java.

Swing och AWT

Jag kommer att tala om två paket som utvecklats av Sun/Oracle för GUI-programmering med Java:

Jag fokuserar på Swing. Eftersom Swing är baserat på AWT kommer jag även att berätta lite om AWT.

Det finns andra paket, till exempel SWT, som utvecklades av IBM som en del av Eclipse, och JavaFX som utvecklats av Sun och Oracle.

Målet med avsnitten om Swing är att lära ut grundläggande koncept om GUI-programmering. Jag vill att de saker jag lär ut ska vara meningsfulla både idag och många år framåt. De moment i GUI-programmering som jag tar upp (till exempel hur man ritar en bild och hur man fångar upp interaktion) är sånt som varje grafiskt system måste hantera.

AWT

En äldre och primitivare lösning.

Swing

Efterhand blev det tydligt att AWT inte var tillräckligt för de krav som användarna ställde. AWT hade begränsningar både i funktionalitet och utseende.

Swing lånar en hel del funktionalitet från AWT, så klasserna i Swing ärver från klasser i AWT. Samtidigt har Swing motsvarigheter till många klasser som definieras i AWT. Det finns till exempel en klass Dialog i AWT och en klass JDialog i Swing. Båda klasserna representerar dialogrutor. Klasser i Swing har i allmänhet namn som börjar på bokstaven 'J'.

Swing är helt skrivet i Java och definierar motsvarigheter till nästan alla AWTs komponenter.

Grafiska användarinterface (GUI)

Vad består ett GUI av?

När vi använder en dator ser vi

Användaren kommunicerar med fönstersystemet via klick och musrörelser eller genom att använda tangentbordet.

Användaren kan flytta fönster på skärmen. Detta påverkar vanligtvis ej applikationen.

Användaren kan kommunicera med programmet genom att göra val på menyer, klicka på knappar, fylla i textrutor etc.

Varje operation som användaren utför kan få programmet att göra olika saker.

En mycket viktig egenskap hos ett välskrivet program med grafiskt användargränssnitt är att användaren bestämmer i vilken ordning olika operationer ska utföras.

(Ibland ser man motsatsen: en applikation som är skriven så att programmet styr användaren, men det är väl inte så vi vill ha det?)

Design av ett GUI: Två uppgifter

Enkelt uttryckt har ett GUI två uppgifter:

Komponenter och behållare

I AWT skiljer man på komponenter som representerar olika GUI-objekt, och behållare (containers) som kan innehålla andra komponenter. I AWT är klassen Container en subklass till Component.

I Swing kan alla komponenter fungera som behållare.

Komponenter

Det mesta du ser i ett GUI är antingen komponenter eller strukturer som är sammansatta av komponenter. I Swing är varje komponent ett objekt i någon subklass till JComponent.

Några exempel på komponenter:

JLabel — (etiketter) visar en enkel text eller bild utan möjlighet till interaktion.

JButton — en knapp som användaren kan klicka på för att få en viss handling utförd. Exempel: Knapparna med texten "OK" resp "cancel" som visas i olika dialoger.

JRadioButton liknar JButton men en JRadioButton är grupperad tilsammans med några andra "radioknappar" i en ButtonGroup. Knapparna i en ButtonGroup representerar olika alternativ där man bara kan välja ett.

JScrollBar — ett skjutreglage av den typ som används för att scrolla när en text är för lång för att få plats på skärmen. Den kan också användas för att välja ett värde i ett intervall.

JMenuBar — en menyrad som man kan placera längst upp i ett fönster. Använd objekt av klassen JMenu för att konstruera en meny. När användaren klickar på en JMenu visas en JPopmenu där ett antal JMenuItem visas. (Skansholm ger exempel på användning av menyer i avsnitt 14.1.)

Swing har tre klasser för att hantera redigering av text: JEditorPane, JTextArea och JTextField. De har en gemensam superklass JTextComponent. JTextField används för inmatning av en enstaka rad text. JTextArea används för att visa och editera en större textarea. Skansholm ger ett enkelt exempel på en editor som använder JTextArea i Sektion 6.13. JEditorPane implementerar ett ramverk som används om man vill bygga mer komplexa editorer.

Operationer på komponenter

Några exempel på operationer som är meningsfulla för de flesta komponenter.

Behållare på toppnivå

Vissa behållare kan placeras på skärmen. Övriga grafiska komponenter kan endast visas när de är placerade i en annan behållare (som antingen är placerad på skärmen, eller i sin tur placerad i en behållare).

Exempel på behållare som kan ses separat:

Andra behållare

JPanel: används för att bygga upp komplicerad layout. Används i BallWorld.

JScrollPane: som JPanel, men kan vara större inuti än utanpå!

Båda är subklasser till JComponent, som i sin tur är en subklass till Container.

Några typiska operationer för att hantera komponenter (dessa är definierade i JComponent):

Enkelt exempel

Låt oss titta på ett enkelt exempel. Jag rekommenderar att ni testkör samtliga exempel, men jag kommer att lägga upp skärmbilder för en del av dem.

Den här klassen ärver av JFrame. Vid programmering av Swing brukar man göra så, även när det inte är helt nödvändigt.

Konstruktorn börjar med att anropa superklassens konstruktor med strängen "Super ...". Metodanropet setBounds(...) bestämmer fönstrets storlek samt placering på skärmen. setDefaultCloseOperation(...) talar om vad som ska hända när fönstret stängs. Till sist talar vi om att fönstret ska vara synligt.

Metoden main skapar en instans av klassen.

import java.awt.*;
import javax.swing.*;

public class Simple extends JFrame {
    public Simple () {
	super("Super simple frame");

	setBounds(100, 150, 400, 100);

	setDefaultCloseOperation(EXIT_ON_CLOSE);
	setVisible(true);
    }

    static public void main(String[] args) {
	new Simple ();
    }
}

Så här kan det se ut när man kör programmet under Ubuntu:

Bakom skymtar ett Emacs-fönster med programmets källkod.

Enkelt dialogfönster

Skansholm förklarar det här exemplet i Kapitel 1.

import javax.swing.*;

public class Meddelande {
  public static void main (String [] args) {

    JOptionPane.showMessageDialog(null,
              "Detta är ett meddelande.");
   }
}

Etikett, exempel

Ett exempel på användning av JLabel (etikett). Vi skapar två objekt av klassen JLabel, ett med texten "Hej", och ett med en bild av ett får. Bilden av fåret finns i en fil med namnet "sheep.gif".

Ibland kan det vara knepigt att få Javas runtime-system att hitta filen. Om filen inte hittas får du inget felmeddelande men bilden kan (förstås) inte visas.

import java.awt.*;
import javax.swing.*;

public class Etikett extends JFrame {
    public Etikett () {
	super("Etiketter");
	ImageIcon icon = new ImageIcon("sheep.gif");
	JLabel hej = new JLabel("Hej");
        JLabel sheep = new JLabel(icon);
	add("West", hej);
	add("East", sheep);
	setBounds(100, 150, 400, 100);
	setDefaultCloseOperation(EXIT_ON_CLOSE);
	setVisible(true);
    }
    static public void main(String[] args) {
	new Etikett ();
    }
}

Ett körexempel. Notera placeringen av de två etiketterna.

Textfält och knappar

Klasser för textfält och knappar:

Notera: vi har ännu inte diskuterat hur man får programmet att reagera på interaktion.

[...]
public class Inmatning1 extends JFrame {

  Inmatning1() {
    super("Inmatning 1");

    JPanel pane = new JPanel();
    JLabel prompt = new JLabel(" Enter a number: ");
    JTextField answer = new JTextField ("", 5);
    JButton pressme = new JButton("Press Me");

    pane.add(prompt);
    pane.add(answer);
    pane.add(pressme);
    add(pane);

    [...]
}

Körexempel:

Färger, fonter och ramar

Några klasser och gränssnitt från AWT som styr utseende och utsmyckning.

Color

Klassen definierar (bland annat) konstanter för olika färger. Man kan till exempel skriva Color.BLUE eller Color.blue.

Enligt Javas namngivningskonvention (läs mer om den här) ska konstanter skrivas med stora bokstäver. Jag tror att klassen Color definierades före man införde denna konvention och att det är därför som alla konstanter för färger kan skrivas på två sätt.

Font

Denna klass definierar olika fonter. Exempel:

new Font("SansSerif", Font.BOLD, 24)

Detta uttryck "skapar" en font av typen sans serif i fetstil av storleken 24 punkter.

Border

Border är ett gränssnitt (interface). Ett exempel på en klass som implementerar Border är BevelBorder (ram med snedkant).

Programmet Smakfull.java (namnet är förstås ironiskt) ger exempel på etiketter med olika dekorationer. Med metoden setOpaque() kan man bestämma om en etikett ska vara ogenomskinlig (true) eller genomskinlig (false). I det senare fallet kommer den underliggande komponententens färg att lysa igenom (och bakgrundsfärgen visas ej).

Här har vi källkoden för de två första knapparna. Notera att den första knappen är ogenomskinlig medan den andra är genomskinlig.

Jag har varit avsiktligt inkonsekvent när jag skrivit namn på färger för att visa att man kan skriva på båda sätten. Jag rekommenderar att ni alltid skriver namnen på färger med stora bokstäver.

    JLabel l1 = new JLabel ("Hej", JLabel.CENTER);
    l1.setOpaque(true);
    l1.setBackground(Color.WHITE);
    l1.setForeground(Color.blue);
    l1.setFont(new Font("SansSerif", Font.BOLD, 24));

    JLabel l2 = new JLabel ("Hopp", JLabel.CENTER);
    l2.setOpaque(false);
    l2.setBackground(Color.WHITE);
    l2.setForeground(Color.BLUE);
    l2.setFont(new Font("Serif", Font.ITALIC, 24));
    xl2.setBorder(new EtchedBorder());

Körexempel:

Layout

I Swing kan man placera ut komponenter i ett fönster genom att ange koordinater, men den metod som rekommenderas är att använda layouthanterare som placerar ut komponenterna enligt någon strategi.

Avsikten är att detta ska ge en design som fungerar bra på olika typer av bildskärmar oavsett om skärmen är liten och har låg upplösning eller är stor och har hög upplösning.

Javas standardbibliotek definierar ett gränssnitt Layout som beskriver vilka metoder som ska implementeras av en layouthanterare. En layouthanterare är förstås en klass som implementerar gränssnittet Layout. Gränssnittet Layout (och alla andra klasser och gränssnitt som diskuteras här) finns dokumenterat i Java's standard-API.

BorderLayout

Arbetar med max 5 komponenter. Dessa ska placeras på någon av positionerna

North, West, Center, East, South

där norr är längst upp, väst är till vänster etc. Låt oss betrakta ett första exempel (program/swing-1/VisaBorderLayout.java).

import java.awt.*;
import javax.swing.*;

public class VisaBorderLayout extends JFrame {
    JButton[] knappar;

    public VisaBorderLayout() {
	String[] knappnamn =
	    {"Öster", "Söder", "Väster", "Norr", "Mitten"};

	knappar = new JButton[knappnamn.length];

	for (int i = 0; i<knappnamn.length; i++) {
	    knappar[i] = new JButton(knappnamn[i]);
	    knappar[i].setForeground(Color.blue);
	    knappar[i].setBackground(Color.pink);
	}

	getContentPane().setBackground(Color.yellow);
	setTitle("Visa BorderLayout");
	getContentPane().setLayout(new BorderLayout(5, 10));
	getContentPane().add("East", knappar[0]);
	getContentPane().add("South", knappar[1]);
	getContentPane().add("West", knappar[2]);
	getContentPane().add("North", knappar[3]);
	getContentPane().add("Center", knappar[4]);
	setDefaultCloseOperation(EXIT_ON_CLOSE) ;
    }

    public static void main(String [] args) {
	VisaBorderLayout window = new VisaBorderLayout();
	window.setSize(400,200);
	window.setVisible(true);
    }
}

Programmet placerar fem knappar på en JPanel. Varje knapp placeras i någon av de fem positionerna, och får ett namn som hjälper oss att minnas vilken position.

Testkör programmet! Prova att ändra fönstrets storlek. Vad händer om man gör fönstret bredare eller smalare? Vad händer om man gör fönstret så litet att alla knappar inte får plats?

Ytterligare exempel på borderlayout:

program/swing-1/VisaBorderLayout2.java:

endast knappar i norr, mitten och öster.

program/swing-1/VisaBorderLayout3.java:

Som det första exemplet, men här har jag ersatt knappen i mitten med en JPanel som i sin tur har fem knappar placerade enligt borderlayout.

GridLayout

I exemplet (program/swing-1/VisaGridLayout.java) tar konstruktorn för gridlayout fyra argument:

new GridLayout(4, 3, 10, 20).

Detta innebär att vi placerar maximalt 4x3 komponenter i ett rutnät med fyra rader och tre kolumner. Tredje argumentet, 10, anger det horisontella mellanrummet mellan komponenter och fjärde argumentet mellanrummet i sidled. Så när behållarens storlek har bestämts vet vi också hur mycket plats det finns för varje komponent. Med gridlayout får alla komponenter lika mycket utrymme.

import java.awt.*;
import javax.swing.*;

public class VisaGridLayout extends JFrame {
    JButton[] knappar = new JButton[10];
    public VisaGridLayout() {
	getContentPane().setBackground(Color.yellow);
	setTitle("Visa GridLayout");
	getContentPane().setLayout( new GridLayout(4, 3, 10, 20));

	for (int i = 0; i < knappar.length; i = i + 1) {
	    knappar[i] = new JButton("Knapp " + (i+1));
	    knappar[i].setForeground(Color.blue);
	    knappar[i].setBackground(Color.pink);
	    getContentPane().add(knappar[i]);
	}
	setDefaultCloseOperation(EXIT_ON_CLOSE); }

    public static void main(String[] args) {
	JFrame f = new VisaGridLayout();
	f.setSize(400,200);
	f.setVisible(true);
    }
}

Låt oss betrakta ett körexempel:

Undersök vad som händer när programmets storlek ändras!

FlowLayout

Med flowlayout placeras komponenterna på behållaren ungefär som när text placeras på ett papper. De första komponenterna placeras på rad, från vänster till höger, längst upp. När en rad är full placeras de följande komponenterna på nästa rad. (Man kan också konfigurera flowlayout så att komponenterna placeras från höger till vänster.)

Programmet program/swing-1/VisaFlowLayout.java placerar tio knappar enligt flowlayout.

Ett körexempel:

Notera att knapp nummer 10 är lite bredare—talet tio har ju två siffror.

Låt oss göra fönstret lite smalare:

eller ännu smalare:

Här är fönstret precis så brett att knappar nummer nio och tio inte får plats på samma rad.

Andra layouter

BoxLayout — Radar upp komponenterna horisontellt eller vertikalt.

CardLayout — Komponenterna placeras 'på varandra' så att endast en komponent i taget blir synlig. (Ungefär som spelkorten i kortlek.)