inhoudstafel en auteursrecht
* STER *


5. Swing

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.

5.1 Componenten

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

een drukknop met de tekst 'Een component met coordinaten (x,y)'

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.

button 'Klik mij', tekstvak, label 'Vaste tekst', keuzerondjes 'optie 1' 'optie 2' 'optie 3', aanvinkvakje 'vinken', combobox 'een'

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.

JTextComponent

Een tekstvak. Dit is een abstracte klasse, die dus slechts kan worden geconstrueerd via een subklasse. De twee meest gebruikte subklassen zijn:

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

JCheckBox

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

JComboBox

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

5.2 Opmaakbeheersing

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.

Oefening

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.

Zoeken naar: - Identieke hoofdletters/kleine letters - Omhoog/omlaag - Volgende zoeken - Annuleren
Een dialoog uit Netscape Navigator 4.7

Zoeken naar: - Identieke hoofdletters/kleine letters - Omhoog/omlaag - Volgende zoeken - Annuleren
Onze imitatie

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

5.3 Gebeurtenissen

De verwerking van handelingen van de gebruiker in een grafische interface gebeurt in twee delen:

  1. Een component genereert een gebeurtenis;
  2. een afhandelaar voert de nodige reacties op de gebeurtenis uit.

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.

klassesoort gebeurtenistypische 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:

  1. Definieer een klasse die een listener interface implementeert;
  2. construeer een object van die klasse;
  3. registreer dat object als listener voor de juist component, meestal met een methode van de vorm addXXXListener

Voorbeeld

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

Achtergrond zwart - Achtergrond wit (2 knoppen)
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);
  }
}

5.4 Grafische bewerkingen

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é


inhoudstafel en auteursrecht
* STER *

Valid HTML 4.0! Valid CSS!