Skansholm: Kapitel 6-8.
Se även Oracles tutorial. (Notera: vi kommer inte att titta på JavaFX.)
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.
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.
En äldre och primitivare lösning.
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.
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?)
Enkelt uttryckt har ett GUI två uppgifter:
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.
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.
Några exempel på operationer som är meningsfulla för de flesta komponenter.
setbackground(Color)
Bestäm bakgrundsfärg.setForeground(Color)
Bestäm förgrundsfärg.setVisible(bool)
Gör komponenten synlig eller osynlig.setFont(f)
Välj typsnitt för komponenten.repaint()
Tala om att det är dags att rita om komponenten.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:
JFrame
: vanliga fönster, med titelrad etc. Normalt är det denna
klass som används när en applikation skapar ett fönster.JWindow
: ett fönster helt utan dekorationer som titelrad etc.JDialog
: ett dialogfönsterJPanel
: 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
):
add(c)
Addera en komponent c
till denna behållare.setLayout(l)
Ge denna behållare layout l
. (Jag tar upp layout
senare i detta avsnitt.)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.
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."); } }
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.
Klasser för textfält och knappar:
JTextField
— för inmatning av en textradJButton
— en knappNotera: 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:
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:
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.
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.
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!
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.
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.)