inhoudstafel en auteursrecht
* STER *
Swing is de populaire verzamelnaam voor een reeks pakketten voor het programmeren van een grafische gebruikersinterface in Java.
Bij het ontwerp van een toepassing met een grafische interface wordt vaak een onderscheid gemaakt tussen twee aspecten:
Voor het ontwerp van de interface bespreken we eerst kort het gebruik van componenten, dat zijn de bouwstenen van de interface, en de opmaak, dat is de onderlinge rangschikking van de componenten in een venster.
Een impuls van de gebruiker noemen we een gebeurtenis. Paragraaf 5.3 gaat over hoe een programma adequaat kan reageren op gebeurtenissen.
In paragraaf 5.4 behandelen we kort hoe het programma eigen grafische elementen kan aanmaken buiten de gewone componenten: tekeningen en afbeeldingen.
Een grafische gebruikersinterface is steeds hiërarchisch gestructureerd. De applicatie manifesteert zich aan de gebruiker als een of meer vensters van het hoogste niveau (Eng. "top level windows"), dat zijn de dingen die wij als gebruikers gewoonlijk "vensters" hebben genoemd. Ze zijn meestal voorzien van een rand, een titelbalk en enkele knoppen. Meestal kan de gebruiker ook de afmetingen van deze vensters wijzigen door de randen of de hoeken te slepen.
Een venster van het hoogste niveau bestaat echter voor het grootste deel uit een functionele ruimte, haar inhoudsvak of inhoudspaneel (Eng. "content pane"). Dit vak bevat componenten die op een bepaalde manier onderling gerangschikt zijn. Iedere component heeft een positie en een stel afmetingen.
Voorbeelden van componenten zijn: drukknoppen, tekstvakken, schuifbalken, keuzelijsten, keuzerondjes, aanvinkvakjes,...
De meeste componenten zijn min of meer rechthoekig. Hun positie wordt aangegeven door de horizontale en de verticale coördinaat van de linkerbovenhoek. Horizontale coördinaten worden vanaf de linkerrand van het inhoudsvak naar rechts gemeten. Verticale coördinaten worden vanaf de bovenrand van het inhoudsvak naar onderen gemeten. Beide coördinaten worden uitgedrukt in beeldpunten (Eng. "pixels").
Sommige componenten kunnen op hun beurt andere componenten bevatten. We noemen ze containers. Een paneel is een voorbeeld van een container. Omdat panelen op hun beurt componenten zijn, kan een paneel andere panelen bevatten. De boomstructuur die op die manier ontstaat, noemen we de bevattingshiërarchie (Eng. "containment hierarchy").
In Java zijn een groot aantal klassen beschikbaar voor
standaard grafische componenten. De meesten behoren
tot de pakketten java.awt en
javax.swing, dus onze programmabestanden
beginnen vaak met de regels
import java.awt.*; import javax.swing.*;
Enkele belangrijke klassen zijn:
JFrame
Een venster van het hoogste niveau. Iedere niet-triviale
Javatoepassing met een grafische interface definieert
minstens één subklasse van JFrame.
De verschillende componenten zijn dan meestal private
attributen van deze subklasse. De main-methode
van de toepassing situeert zich al dan niet in deze
subklasse: in elk geval zal ze een instantie van deze
subklasse construeren.
Met de methode getContentPane() krijgen
we een Container terug, zodat we componenten
kunnen toevoegen aan het venster (zie hieronder bij
JPanel).
Voor een voorbeeld van deze en volgende grafische componenten in een programma verwijzen we naar het einde van paragraaf 5.1. Hier is alvast het uitzicht van het venster van deze voorbeeldtoepassing. Het bevat: een paneel (blauw) met een drukknop, een vaste tekst op een label, drie onderling gekoppelde keuzerondjes, een aanvinkvakje en een combobox.
JPanel
Een "gewone" container, zonder randen of knoppen. Deze klasse
erft haar belangrijkste methode van haar grootmoeder
Container:
public Component add(Component comp);
Met deze methode wordt een component aan het paneel toegevoegd.
De onderlinge rangschikking van de componenten binnen
één paneel wordt bepaald door een
LayoutManger: zie paragraaf 5.2.
Voorlopig beperken we ons ertoe, voor de eerste
aanroep van add de volgende code
te programmeren:
eenPaneel.setLayout(new FlowLayout());
waardoor de componenten min of meer van links naar rechts en van boven naar onder worden gerangschikt, in de volgorde waarin we ze aan het paneel toevoegen.
JButton
Een drukknop. Het opschrift kan worden gewijzigd met de methode
setText. Het bestaande opzicht kan je raadplegen met
de methode getText. Er bestaat een constructor die
als parameter reeds een tekst aanvaardt, zodat je de eerste aanroep
van setText kan uitsparen.
JTextComponentEen tekstvak. Dit is een abstracte klasse, die dus slechts kan worden geconstrueerd via een subklasse. De twee meest gebruikte subklassen zijn:
JTextField, voor een tekstvak met een korte tekst
(op één regel)JTextArea voor een tekstvak dat eventueel verschillende
regels beslaatBeiden hebben constructoren met als parameter resp. een tekst (de aanvankelijke inhoud), niets (een leeg tekstvak), of een getal (een leeg tekstvak waarvan de afmetingen voorzien zijn op ongeveer het gegeven aantal tekens).
De inhoud kan geraadpleegd worden met de methode getText
en gewijzigd met de methode setText.
JLabel
Een onveranderlijke tekst die niet de toetsenbord-focus kan ontvangen.
Wordt typisch op een doorschijnende achtergrond weergegeven, zodat de
achtergrondkleur gelijk is aan die van het onderliggende paneel. Zoals
bij JTextField kan ook hier de inhoud geraadplaagd resp.
gewijzigd worden met de methoden getText en setText.
JRadioButton
Een selectieknop die aan of uit kan staan. Meestal worden radioknoppen
afgebeeld als een groep "keuzerondjes. Het inschakelen van de ene selectie
heeft tot gevolg dat de vorige selectie ongedaan gemaakt wordt.
Gebruik de constructor om een radioknop van een tekst te voorzien.
Radioknoppen worden gegroepeerd door een object van het type JButtonGroup.
Roep van dat object de methode add aan met als argument
een radioknop. Herhaal dit voor alle radioknoppen die bij elkaar horen.
JCheckBoxEen selectieknop die aan of uit kan staan, onafhankelijk van andere selecties. Wordt meestal afgebeeld als een vierkant hokje waarin de gebruiker met de muis een kruisje of een vinkje kan aanbrengen. De constructor aanvaardt een tekstparameter voor het begeleidende opschrift.
JComboBoxEen keuzelijst die slechts één optie tegelijk laat zien, tenzij de gebruiker rechts op een pijltjesknop drukt.
Hier is de broncode van het beloofde voorbeeldprogramma waarin de meeste van de hogergenoemde componenten aan bod komen.
import java.awt.*;
import javax.swing.*;
/** Deze toepassing toont een voorbeeld van
* enkele componenten uit de klassenbibliotheek
* Swing.
*/
class ComponentenVoorbeeld extends JFrame {
/** Een paneel waarop we een afzonderlijke knop
* aanbrengen.
*/
JPanel kleinPaneel = new JPanel();
/** Een drukknop met het opschrift "Klik mij". */
JButton btnKlik = new JButton("Klik mij");
/** Een klein, leeg tekstvak. */
JTextField txtKlein = new JTextField(20);
/** Een label met onveranderlijke tekst. */
JLabel lblTitel = new JLabel("Vaste tekst");
/** Een logische groepering van drie radioknoppen. */
ButtonGroup grpDrie = new ButtonGroup();
/** Radioknop met de tekst 'optie 1', aanvankelijk
* in de stand 'aan'.
*/
JRadioButton btnOptie1 = new JRadioButton("optie 1", true);
/** Radioknop met de tekst 'optie 2' . */
JRadioButton btnOptie2 = new JRadioButton("optie 2");
/** Radioknop met de tekst 'optie 3' . */
JRadioButton btnOptie3 = new JRadioButton("optie 3");
/** Een aanvinkvakje met de tekst 'vinken'. */
JCheckBox chkVinken = new JCheckBox("vinken");
/** Een combobox met als keuze-elementen 'een',
* 'twee' en 'drie'.
*/
JComboBox cmbDrie;
/** Construeer een venster met enkele componenten.
* Stel zijn afmetingen in en maak het zichtbaar.
*/
ComponentenVoorbeeld() {
super("Enkele Swingcomponenten");
Container c = getContentPane();
c.setLayout(new FlowLayout());
kleinPaneel.setLayout(new FlowLayout());
kleinPaneel.add(btnKlik);
kleinPaneel.setBackground(Color.blue);
c.add(kleinPaneel);
c.add(txtKlein);
c.add(lblTitel);
grpDrie.add(btnOptie1);
grpDrie.add(btnOptie2);
grpDrie.add(btnOptie3);
c.add(btnOptie1);
c.add(btnOptie2);
c.add(btnOptie3);
c.add(chkVinken);
String[] drieTeksten = { "een", "twee", "drie" };
cmbDrie = new JComboBox(drieTeksten);
c.add(cmbDrie);
setSize(300, 200);
show();
}
/** Construeer het hoofdvenster. */
public static void main(String[] args) {
new ComponentenVoorbeeld();
}
}
Met ieder object van het type Container wordt een
opmaakbeheerder geassocieerd, een object van het
interface-type LayoutManager. De opmaakbeheerder
bepaalt dynamisch de positie en de afmetingen van de componenten
die in de container zijn geplaatst, rekening houdend met:
Daarbij moeten compromissen gesloten worden tussen wat
wenselijk is en wat haalbaar is, volgens vaste regels. Iedere
concrete klasse die de interface LayoutManager
implementeert, bepaalt de rangschikking volgens zijn eigen
regels.
Zelf dergelijke klassen implementeren valt buiten het bestek van deze tekst. We zullen hier gebruikmaken van de bestaande opmaakbeheerders uit de klassenbibliotheek:
java.awt.FlowLayout java.awt.BorderLayout java.awt.GridLayout java.awt.GridBagLayout java.awt.CardLayout javax.swing.BoxLayout
Door de methode setLayout van de klasse Container
wordt met een gegeven container een opmaakbeheerder geassocieerd. Sommige klassen,
zoals JPanel, laten ook een opmaakbeheerder toe als parameter
bij de constructor.
Bij een FlowLayout worden de componenten met hun kleinst bruikbare
afmetingen gerangschikt van links naar rechts, bovenaan in de container. Als
er niet voldoende breedte beschikbaar is, worden componenten verplaatst naar de
volgende lijn. De componenten die op die manier op dezelfde hoogte belanden,
worden als groep gecentreerd tussen de linker- en de rechterrand van de container.
Compileer een toepassing met een FlowLayout waarop verschillende componenten
staan (bijvoorbeeld ComponentenVoorbeeld.java hierboven). Laat
de toepassing lopen. Experimenteer met het verbreden, versmallen, verhogen
en verlagen van het venster. Hoe gaat een dergelijke opmaakbeheerder om met
een gebrek aan plaats in de hoogte ?
Een BorderLayout is uitsluitend geschikt om ten hoogste vijf
componenten te rangschikken. De vijf beschikbare posities heten, respectievelijk,
"North", "South", "East", "West" en "Center". Je kan de componenten niet
met de gewone methode add aan de container toevoegen; gebruik
in plaats daarvan de methode add met twee parameters: een
component, gevolgd door een symbolische constante die de positie aangeeft.
De mogelijke constanten zijn:
BorderLayout.NORTH BorderLayout.SOUTH BorderLayout.EAST BorderLayout.WEST BorderLayout.CENTER
Uiteraard hoeven niet allevijf de posities daadwerkelijk te worden opgevuld.
Een BorderLayout wordt vaak gebruikt in een containment hierarchy:
de componenten in de ene container zijn dan op hun beurt containers (bijvoorbeeld
van het type JPanel) die andere componenten bevatten. Daarbij heeft
elke container zijn eigen opmaakbeheersobject.
Een GridLayout rangschikt de componenten in een rooster van
een gegeven aantal rijen en kolommen. De rijen zijn even hoog, de kolommen
zijn even breed. Iedere component wordt ingekrompen of uitgerokken om precies
de beschikbare ruimte van één cel in te nemen.
Een GridBagLayout borduurt hierop voort. Hij biedt meer flexibiliteit
ten koste van een beetje meer programmeerwerk:
Een CardLayout laat toe, verscheidene overlappende containers
te definiëren om er telkens één van de laten zien.
Een BoxLayout is erg flexibel en tamelijk complex in het
gebruik. Hij rangschikt de componenten zoals een zetter in een drukkerij
de elementen van een bladzijde rangschikt op de matrijs: afwisselend in
horizontale en verticale boxes.
We sluiten deze paragraaf af met een voorbeeld dat aantoont hoe
verschillende eenvoudige opmaakbeheerders kunnen gecombineerd worden
tot een aantrekkelijk en overzichtelijk geheel. Daarbij wordt ook gebruik
gemaakt van randen, dat zijn objecten die de interface
Border implementeren. Een rand wordt aangemaakt door
statische methoden van de klasse BorderFactory; met de
methode setBorder geef je een paneel een bepaalde
rand.
Hier volgt dan de broncode.
import java.awt.*;
import javax.swing.*;
class ComplexeDialoog extends JFrame {
JPanel pnlBoven = new JPanel(new FlowLayout()),
pnlOnder = new JPanel(new BorderLayout()),
pnlLinks = new JPanel(new BorderLayout()),
pnlRechts = new JPanel(new GridLayout(3, 1, 0, 7)),
pnlRadio = new JPanel(new GridLayout(1, 2));
JLabel lblZoeken = new JLabel("Zoeken naar:");
JTextField txtZoeken = new JTextField(25);
JButton btnVolgende = new JButton("Volgende zoeken"),
btnAnnuleren = new JButton("Annuleren");
JCheckBox chkIdentiek = new JCheckBox(
"Identieke hoofdletters/kleine letters");
JRadioButton radOmhoog = new JRadioButton("Omhoog"),
radOmlaag = new JRadioButton("Omlaag", true);
ComplexeDialoog() {
super("Zoeken");
Container c = getContentPane();
c.setLayout(new BorderLayout());
c.add("North", pnlBoven);
c.add("Center", pnlOnder);
pnlBoven.add(lblZoeken);
pnlBoven.add(txtZoeken);
pnlOnder.add("Center", pnlLinks);
pnlOnder.add("East", pnlRechts);
pnlRechts.add(btnVolgende);
pnlRechts.add(btnAnnuleren);
pnlRechts.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
pnlLinks.add("Center", chkIdentiek);
pnlLinks.add("South", pnlRadio);
pnlLinks.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
pnlRadio.add(radOmhoog);
pnlRadio.add(radOmlaag);
pnlRadio.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(), "Richting"));
ButtonGroup grpRichting = new ButtonGroup();
grpRichting.add(radOmhoog);
grpRichting.add(radOmlaag);
setSize(380, 170);
show();
}
public static void main(String[] args) {
ComplexeDialoog myFrame;
myFrame = new ComplexeDialoog();
}
}
De verwerking van handelingen van de gebruiker in een grafische interface gebeurt in twee delen:
Componenten hebben we reeds ontmoet in paragraaf 5.1.
Afhandelaars of event handlers zijn objecten wier klasse
een zgn. listener interface implementeert. Swing kent
verschillende soorten listeners, naargelang van het type van
de gebeurtenis of gebruikershandeling. We geven hier in enkele
voorbeelden in alfabetische volgorde. De meest gebruikte staan
in vetjes. Alle listener interfaces zijn afgeleid van
java.util.EventListener.
| klasse | soort gebeurtenis | typische componenten |
|---|---|---|
java.awt.event.ActionListener |
actie: druk op een knop, selectie van een menu-item, ENTER drukken in een kort tekstveld | JButton, JMenuItem, JTextField |
java.awt.event.AdjustmentListener |
wijziging in de stand van een schuifbalk | JScrollBar |
javax.swing.event.AncestorListener |
een verandering in de plaatsing van een component (toevoegen aan of verwijderen uit container, nieuwe afmetingen, zichtbaar of onzichtbaar worden,...) | JComponent |
javax.swing.event.CaretListener |
wijziging van de plaats van de tekstcursor | JTextComponent |
java.awt.event.ComponentListener |
wijzigingen in de plaats en rangschikking der componenten in een container | java.awt.Container en dus ook JComponent |
java.awt.event.ContainerListener |
wijzigingen in de verzameling componenten die een container bevat | java.awt.Container en dus ook JComponent |
javax.swing.event.DocumentListener |
de inhoud van een tekstvak verandert | javax.swing.text.Document: het document
dat bij een JTextComponent hoort, kan je opvragen
met de methode getDocument() |
java.awt.event.FocusListener |
een component heeft de focus verworven of afgestaan; focus is het vermogen, toetsaanslagen te verwerken | JComponent |
java.awt.event.ItemListener |
de gebruiker heeft een selectie van het type 'aan/uit' gemaakt | JMenuItem (als er een vinkje bij kan staan),
JCheckBox, JRadioButton |
java.awt.event.KeyListener |
toetsaanslag | JComponent |
javax.swing.event.ListSelectionListener |
de gebruiker kiest een nieuw element uit een lijst | JList |
java.awt.event.MouseListener |
de gebruiker verricht een muishandeling die meer is dan alleen maar een beweging: op een knop drukken, een knop loslaten, of door beweging het gebied van een component binnenkomen of verlaten | JComponent |
java.awt.event.MouseMotionListener |
de muis beweegt | JComponent |
java.awt.event.WindowListener |
een venster verandert van toestand (minimaal, maximaal, gewoon, activeren, desactiveren, openen, sluiten) | java.awt.Window en dus ook
JFrame, JDialog en JWindow |
Een listener is een interface, dus eigenlijk een lege doos. Slechts de
naam, de parameter-signatuur en het terugkeertype (void) van
de methoden ligt vast. Om een component in de gebruikersinterface actief
te maken, ga je als volgt te werk:
addXXXListenerHet volgende programma presenteert de gebruiker een knop. Als de gebruiker de knop indrukt, verandert de achtergrondkleur.
Het indrukken van de knop is een gebeurtenis van het type ActionEvent,
we moeten dus een klasse definiëren die de interface ActionListener
implementeert.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class KnopAfhandelaar implements ActionListener {
Container teKleuren;
KnopAfhandelaar(Container c) {
teKleuren = c;
}
public void actionPerformed(ActionEvent e) {
teKleuren.setBackground(Color.black);
}
}
class KleurWijziging extends JFrame {
JButton deKnop = new JButton("Achtergrond zwart");
KleurWijziging(String titel) {
super(titel);
Container c = getContentPane();
c.setLayout(new FlowLayout());
c.add(deKnop);
/* Registreer een object van het type KnopAfhandelaar
voor het afhandelen van een druk op de knop.
*/
deKnop.addActionListener(new KnopAfhandelaar(c));
}
public static void main(String[] args) {
KleurWijziging k = new KleurWijziging("Demo ActionListener");
k.setSize(300, 200);
k.show();
}
}
In bovenstaand voorbeeld krijgt de constructor van KnopAfhandelaar
een referentie mee naar een Container: anders zou hij niet in staat
zijn de achtergrondkleur ervan te veranderen. Als een afhandelaar veel informatie
moet hebben over de interne keuken van een venster of container, is het soms
beter en overzichtelijker als die container of dat venster zelf de overeenkomstige
listener interface implementeert.
Veronderstel dat we bijvoorbeeld een tweede knop "Achtergrond wit" aan bovenstaande
toepassing willen toevoegen. Ofwel moeten we daarvoor een afzonderlijke afhandelaar-klasse
schrijven, wat wegens codeduplicatie geen goede ontwerpstrategie lijkt, ofwel moet
de methode actionPerformed het onderscheid kunnen maken tussen de
twee knoppen. Een en ander wordt eenvoudiger te programmeren als we alles
in één klasse onderbrengen.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
class KleurWijziging2 extends JFrame implements ActionListener {
JButton btnZwart = new JButton("Achtergrond zwart"),
btnWit = new JButton("Achtergrond wit");
KleurWijziging2(String titel) {
super(titel);
Container c = getContentPane();
c.setLayout(new FlowLayout());
c.add(btnZwart);
c.add(btnWit);
/* Registreer het huidige venster als afhandelaar
van het indrukken van een van de twee knoppen.
*/
btnZwart.addActionListener(this);
btnWit.addActionListener(this);
}
public static void main(String[] args) {
KleurWijziging2 k = new KleurWijziging2("Demo ActionListener");
k.setSize(300, 200);
k.show();
}
public void actionPerformed(ActionEvent e) {
Object bronDerGebeurtenis = e.getSource();
// Maak het onderscheid tussen twee knoppen
if (bronDerGebeurtenis.equals(btnZwart))
getContentPane().setBackground(Color.black);
else if (bronDerGebeurtenis.equals(btnWit))
getContentPane().setBackground(Color.white);
}
}
De Java Development Kit voorziet ook grafische bewerkingen voor het lezen of tekenen van afbeeldingen.
De sleutel tot het manipuleren van grafische informatie is de grafische apparaatcontext
("Graphics device context"), gemodelleerd door de abstracte klasse java.awt.Graphics
of haar abstracte dochterklasse java.awt.Graphics2D.
Elke Swing-component implementeert de methode
public void paint(Graphics g)
die een parameter van dat type meekrijgt. Deze methode moet de component grafisch weergeven op de
device context g en bepaalt als dusdanig de uiterlijke verschijning van de component.
Het systeem (in principe, de Java Virtual Machine) bepaalt wanneer de methode paint wordt
aangeroepen. Zelf de methode paint aanroepen wordt als slechte programmeerpraktijk beschouwd.
In zekere zin gedraagt paint zich dus als een gebeurtenis-afhandelaar. Het enige verschil is
dat de gebeurtenis niet rechtstreeks wordt ontketend door een gebruikershandeling, maar door een beslissing
van de werkomgeving (met name, dat het tijd is om de component te tekenen of te hertekenen).
De eenvoudigste manier om zelf een tekening te programmeren bestaat erin, een subklasse te definiëren
van een bestaande component, en de methode paint te herdefiniëren. De klasse JPanel
is hiervoor uitermate geschikt omdat haar standaardmethode paint toch al niet veel
interessants deed.
De klasse Graphics beschikt over een groot aantal methoden om grafische primitieven te
tekenen. De eenvoudigste is drawLine, die een lijnstuk tekent waarvan begin- en eindpunt
worden uitgedrukt als (x,y)-coördinaten.
public abstract void drawLine(int x1, int y1, int x2, int y2)
De kleur van de lijn kan je beïnvloeden door vooraf de methode setColor
aan te roepen. De enige parameter van het type java.awt.Color kan je construeren met
drie gehele getallen van 0 tot 255 die respectievelijk de intensiteit in rood, groen
en blauw weergeven.
In voorbereiding: voorbeeld
In voorbereiding: voorbeeld met button om de kleur te veranderen -> heeft repaint() nodig
In voorbereiding: eenvoudig tekenprogramma met muis
In voorbereiding: gesofisticeerde tekeningen met Graphics2D, o.a. antialias, gradé