Helsingin yliopisto / Tietojenkäsittelytieteen laitos / 581258-1 Johdatus ohjelmointiin
Copyright © 1998 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

Graafisen käyttöliittymän toteuttamisesta

(Muutettu viimeksi 26.1.1998)

Idea

Javalla graafisen käyttöliittymän voi ohjelmoida hyvin monella tavalla. Erityisesti tapahtumien käsittelyn ohjelmointiin on erilaisia strategioita.

Tässä ohjeessa esitellään muutamia yksinkertaisia tapoja pienen sovelluksen toteuttamiseen.

Ratkaisut perustuvat käyttöliittymän ja tietorakenteen erottamiseen toisistaan. Käyttöliittymäluokka on sovelluksen "pääohjelmaluokka", joka käyttää abstraktia tietorakennetta (kts. Johdatus ohjelmointiin, luku 4.3). Abstrakti tietotyyppi on toteutettu luokkana, joka kätkee tietorakenteen konkreettisen toteutuksen ja tarjoaa operaatioita arvojen muutteluun ja kyselemiseen.

Sovelluksen pääohjelmaluokka siis

Esimerkkisovelluksen määrittely

Esimerkkisovellus on tarkoituksella äärimmäisen yksinkertainen, jotta idea ei hautautuisi yksityiskohtiin!

Tehtävä: Toteuta graafiseen käyttöliittymään perustuva sovellus Laskurisovellus, jolla käyttäjä voi laskea nappulan painalluksia. Tällaista laskuria voisi käyttää vaikkapa lampaiden laskemiseen: paina nappulaa aina kun lammas hyppää aidan yli ... Laskuri täytyy myös voida asettaa uudelleen alkutilaan. Laskurilla on jokin vakioyläraja, jota ei saa ylittää. Esimerkeissä yläraja on 10 (rajaa on helppo muuttaa).

Sovelluksen toiminnot:

Abstrakti tietotyyppi Laskuri

Abstrakti tietotyyppi siis kätkee tietojen säilytystavan ja tietojen käsittelyalgoritmit tyypin käyttäjältä. Tyyppi Laskuri tarjoaa seuraavat operaatiot:

Huom: Abstraktin tietotyypin hyvä suunnittelu - mitä ja millaisia operaatioita - on hyvin oleellinen ja kriittinen tehtävä! Se suurelta osin määrää koko ratkaisun selkeyden ja tyylikkyyden. Jos tietotyyppiä käytettäessä tulee halu päästä käsiksi toteutuksen yksityiskohtiin, operaatiot on luultavasti määritelty epätarkoituksenmukaisesti! Tietotyypin toteutus, Luokka Laskuri.java:

////////////////////////////////////////////// Arto Wikla 1998 ///////////
//
//              Luokka Laskuri: toteuttaa ylöspäinlaskurin.
//
//   Konstruktori:
//
// public Laskuri(int raja)
// luo Laskurin joka aloittaa nollasta ja jonka viimeinen sallittu arvo
// annetaan parametrina. Jos parametri on negatiivinen, luodaan Laskuri,
// jonka viimeinen sallittu arvo on nolla
//
//   Aksessorit:
//
// public int mikaArvo()
// palauttaa Laskurin arvon.
//
// public boolean lopussa()
// palauttaa tiedon siitä, onko laskurin arvo jo saavuttanut ylärajan,
// arvo on true, jos yläraja on saavutettu
//
//   Operaatiot:
//
// public void kasvata()
// kasvattaa Laskurin arvoa, jos ylärajaa ei ole saavutettu.
//
// public void nollaa()
// nollaa Laskurin arvon.
//
//////////////////////////////////////////////////////////////////////////

public class Laskuri {

  // tietorakenteen toteutuksen muuttujat:

  private final int YLARAJA;  // konstruoitavan laskurin viimeinen arvo
  private int arvo;           // laskurin arvo 0, 1, 2, ..., YLARAJA

  // Konstruktori:

  public Laskuri(int raja) {
    arvo = 0;
    if (raja > 0)
      YLARAJA = raja;
    else
      YLARAJA = 0;
  }

  // Aksessorit:

  public int mikaArvo() { return arvo; }
  public boolean lopussa() { return arvo >= YLARAJA; }

  // Operaatiot:

  public void kasvata() {
    if (!lopussa()) {
      ++arvo;
    }
  }

  public void nollaa() {
    arvo = 0;
  }
}


Sovelluksen pääluokka: käyttöliittymän elementit , lay-out ja tapahtumiin reagointi

Johdatus ohjlemointiin kurssin luku 6.5 esittelee graafisen käyttöliittymän toteuttamisen alkeita. Se on syytä kerrata ennen oman ohjelman suunnittelemista. Luvun sisältö oletetaan tässä esimerkissä tunnetuksi!

Ensimmäisessä versiossa konstruktori kantaa koko vastuun käyttöliittymän elementtien alustuksesta, Laskurisovellus1.java:

//////////////////////////////////////////// Arto Wikla 1998 /////////////
//
// Luokka Laskurisovellus1 tarjoaa käyttäjälle laskurin, joka laskee 
// nappulan painamisia arvoilla 1, ..., 10. Ylärajaa on helppo muuttaa, 
// kts. ohjelman alussa oleva vakio LASKURIN_KOKO.
//
// Sovellus tarjoaa myös mahdollisuuden uudelleenaloitukseen eli 
// laskurin nollaamiseen.
//
// Sovellus käyttää laskurina abstraktin tietotyypin Laskuri tarjoamia
// välineitä. Kts. rajapinnan määrittelyä tiedoston Laskuri.java alussa.
//
// Käännösyksikön lopussa on luokka HoiteleIkkunanSulkeminen, 
// kts. Johdatus ohjelmointiin, luku 6.5.
// 
//////////////////////////////////////////////////////////////////////////
import java.awt.*;
import java.awt.event.*;

public class Laskurisovellus1 extends Frame
                              implements ActionListener {

  private final int LASKURIN_KOKO = 10; // laskurin viimeinen arvo

  private final Laskuri laskuri; // muuttuja abstraktille tietorakenteelle

  // käyttöliittymän elementtikentät:

  private final TextField laskurinArvo;
  private final Button kasvata;
  private final Button nollaa;
  private final Button lopeta;

  //----- sovelluksen konstruointi: ----//

  public Laskurisovellus1() {

    laskuri = new Laskuri(LASKURIN_KOKO);  // tietorakenteen luonti

    // käyttöliittymän elementtien arvot:

    laskurinArvo = new TextField(5);
    laskurinArvo.setText("ALUSSA!");
    laskurinArvo.setEditable(false);

    kasvata = new Button("kasvata");
    nollaa  = new Button("aloita alusta");
    lopeta  = new Button("lopeta");

    // tapahtumankuuntelijoiden asettaminen:

    kasvata.addActionListener(this);
    nollaa.addActionListener(this);
    lopeta.addActionListener(this);

    // näkymän lay-outin luonti:

    add("North", laskurinArvo);
    add("West", kasvata);
    add("East", nollaa);
    add("South", lopeta);

    // ikkunan sulkemisen kuuntelijan asettaminen:

    addWindowListener(new HoiteleIkkunanSulkeminen());
  }

  //----- tapahtumankäsittelijä: -----//

  public void actionPerformed(ActionEvent tapahtuma) {

    Object aiheuttaja = tapahtuma.getSource();

    if (aiheuttaja == kasvata)
      kasvatus();
    else if (aiheuttaja == nollaa)
      nollaus();
    else if (aiheuttaja == lopeta)
      lopetus();
  }

  //----- tapahtumaoperaatiot: -----//
  // (käytetään abstraktin tietotyypin Laskuri tarjoamia operaatioita)

  private void kasvatus() {
    if (!laskuri.lopussa()) {
      laskuri.kasvata();
      laskurinArvo.setText(""+laskuri.mikaArvo());
    }
    else
      laskurinArvo.setText("LOPPU!");
  }

  private void nollaus() {
    laskuri.nollaa();
    laskurinArvo.setText("ALUSSA!");
  }

  private void lopetus() {
    System.exit(0);  // ikkunan sulkeminen
  }

  //----- sovelluksen pääohjelma: -----//

  public static void main(String[] args) {
    Laskurisovellus1 ikkuna = new Laskurisovellus1();
    ikkuna.setTitle(ikkuna.LASKURIN_KOKO+"-laskuri");
    ikkuna.pack();
    ikkuna.setVisible(true);
  }
}

//----- ikkunan sulkemisen hoitelu: -----//

class HoiteleIkkunanSulkeminen extends WindowAdapter {
  public void windowClosing(WindowEvent tapahtuma) {
    System.exit(0);  // ikkunan sulkeminen
  }
}


Vaihtoehtoinen tapa luoda käyttöliittymän elementit

Käyttöliittymän elementit voidaan luoda myös jo määrittelyn yhteydessä, jolloin konstruktorille jää vähemmän tehtävää (Laskurisovellus2.java):

  ...

  private final int LASKURIN_KOKO = 10; // laskurin viimeinen arvo

  private final Laskuri laskuri; // muuttuja abstraktille tietorakenteelle

  // käyttöliittymän elementit:

  private final TextField laskurinArvo = new TextField(5);
  private final Button kasvata = new Button("kasvata");
  private final Button nollaa  = new Button("aloita alusta");
  private final Button lopeta  = new Button("lopeta");

  //----- sovelluksen konstruointi: ----//

  public Laskurisovellus2() {

    laskuri = new Laskuri(LASKURIN_KOKO);  // tietorakenteen luonti

    // laskurinArvo-muuttujan alustus:

    laskurinArvo.setText("ALUSSA!");
    laskurinArvo.setEditable(false);

    // tapahtumankuuntelijoiden asettaminen:

    kasvata.addActionListener(this);
    nollaa.addActionListener(this);
    lopeta.addActionListener(this);

    // näkymän lay-outin luonti:

    add("North", laskurinArvo);
    add("West", kasvata);
    add("East", nollaa);
    add("South", lopeta);


    // ikkunan sulkemisen kuuntelijan asettaminen:

    addWindowListener(new HoiteleIkkunanSulkeminen());
    ...

Toinen vaihtoehtoinen tapa luoda käyttöliittymän elementit

Myös käyttöliittymän elementtien toiminnallisuus voidaan ohjelmoida jo määrittelyn yhteydessä (käytetään ns. ilmentymäalustuslohkoja (instance initializers), "dynaamisia alustuslohkoja", joilla voi alustaa ilmentymämuuttujia (vrt. staattiset alustuslohkot ).

Huom: Ilmentymäalustuslohkot ovat Javan version 1.1.* uutuus, Niitä ei käsitelty kurssilla!)

Laskurisovellus3.java:

  ...
  private final int LASKURIN_KOKO = 10; // laskurin viimeinen arvo

  private final Laskuri laskuri; // muuttuja abstraktille tietorakenteelle

  // käyttöliittymän elementit, niiden alkuarvot ja kuuntelijat:

  private final TextField laskurinArvo = new TextField(5);
    { laskurinArvo.setText("ALUSSA!"); 
      laskurinArvo.setEditable(false); 
    }

  private final Button kasvata = new Button("kasvata");
    { kasvata.addActionListener(this); 
    }

  private final Button nollaa  = new Button("aloita alusta");
    { nollaa.addActionListener(this); 
    }

  private final Button lopeta  = new Button("lopeta");
    { lopeta.addActionListener(this); 
    }

  //----- sovelluksen konstruointi: ----//

  public Laskurisovellus3() {

    laskuri = new Laskuri(LASKURIN_KOKO);  // tietorakenteen luonti

    // näkymän lay-outin luonti:

    add("North", laskurinArvo);
    add("West", kasvata);
    add("East", nollaa);
    add("South", lopeta);

    // ikkunan sulkemisen kuuntelijan asettaminen:

    addWindowListener(new HoiteleIkkunanSulkeminen());
    ...

Huom: Myös lay-out asetukset, elementtien asemointi, voitaisiin tehdä jo ilmentymäalustuslohkoissa, mutta se ei olisi viisasta!

Tapojen vertailua

Nämä kolme tapaa painottavat erilaisia asioita: Ensimmäisessä konstruointi todella konstruoi kaiken, toisessa vakiona pysyvät käyttöliittymän elementit asetetaan jo muuttujien määrittelyn yhteydessä.

Vaihtoehdon valinta on osittain makuasia, osittain se riippuu tarpeista. Jos vaikkapa konstruktorin parametrin tulee vaikuttaa alkuarvoihin, alustukset on tehtävä konstruktorissa. Tehdäänkö siellä silloin kaikki alustukset vai vain tarpeelliset? Asetetaanko oletusalkuarvot määrittelyiden yhteydessä?

Valintavaihtoehtoja on paljon. Selkeys taas kerran ratkaiskoon!

Toisenlaista tapahtumankäsittelyä

Edellisissä esimerkeissä tapahtumat käsitellään keskitetysti metodilla public void actionPerformed(ActionEvent tapahtuma), jonka toteuttaminen luvataan jo luokan otisikossa:
public class Laskurisovellus1 extends Frame
                              implements ActionListener {

(kts. rajapintaluokan ActionListener määrittely)

Javan nimettömillä sisäluokilla (anonymous inner classes) tapahtumankäsittely voidaan ohjelmoida käyttöliittymäelementeittäin, hajautetusti. Tätä tapaa voidaan ehkä pitää "oliohenkisempänä" kuin edellistä.

Toteutetaan Laskurisovellus4.java muokkaamalla ensimmäistä esimerkkiä:

//////////////////////////////////////////// Arto Wikla 1998 /////////////
//
// Luokka Laskurisovellus4 tarjoaa käyttäjälle laskurin, joka laskee
// nappulan painamisia arvoilla 1, ..., 10. Ylärajaa on helppo muuttaa,
// kts. ohjelman alussa oleva vakio LASKURIN_KOKO.
//
// Sovellus tarjoaa myös mahdollisuuden uudelleenaloitukseen eli
// laskurin nollaamiseen.
//
// Sovellus käyttää laskurina abstraktin tietotyypin Laskuri tarjoamia
// välineitä. Kts. rajapinnan määrittelyä tiedoston Laskuri.java alussa.
//
//////////////////////////////////////////////////////////////////////////
import java.awt.*;
import java.awt.event.*;

public class Laskurisovellus4 extends Frame {

  private final int LASKURIN_KOKO = 10; // laskurin viimeinen arvo

  private final Laskuri laskuri; // muuttuja abstraktille tietorakenteelle

  // käyttöliittymän elementtikentät:

  private final TextField laskurinArvo;
  private final Button kasvata;
  private final Button nollaa;
  private final Button lopeta;

  //----- sovelluksen konstruointi: ----//

  public Laskurisovellus4() {

    laskuri = new Laskuri(LASKURIN_KOKO);  // tietorakenteen luonti

    // käyttöliittymän elementtien arvot:

    laskurinArvo = new TextField(5);
    laskurinArvo.setText("ALUSSA!");
    laskurinArvo.setEditable(false);

    kasvata = new Button("kasvata");
    nollaa  = new Button("aloita alusta");
    lopeta  = new Button("lopeta");

    // tapahtumankuuntelijoiden asettaminen ja tapahtumien käsittely:

    kasvata.addActionListener(
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          if (!laskuri.lopussa()) {
            laskuri.kasvata();
            laskurinArvo.setText(""+laskuri.mikaArvo());
          }
          else
            laskurinArvo.setText("LOPPU!");

        }
      }
    );

    nollaa.addActionListener(
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          laskuri.nollaa();
          laskurinArvo.setText("ALUSSA!");
        }
      }
    );

    lopeta.addActionListener(
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          System.exit(0);  // ikkunan sulkeminen
        }
      }
    );

    // näkymän lay-outin luonti: 

    add("North", laskurinArvo);
    add("West", kasvata);
    add("East", nollaa);
    add("South", lopeta);

    // ikkunan sulkemisen kuuntelijan asettaminen ja sulkemisen käsittely:

    addWindowListener(
      new WindowAdapter () {
        public void windowClosing(WindowEvent tapahtuma) {
           System.exit(0);  // ikkunan sulkeminen
        }
      }
    );

  }

  //----- sovelluksen pääohjelma: -----// 

  public static void main(String[] args) {
    Laskurisovellus4 ikkuna = new Laskurisovellus4();
    ikkuna.setTitle(ikkuna.LASKURIN_KOKO+"-laskuri");
    ikkuna.pack();
    ikkuna.setVisible(true);
  }
} 


Huom:

Huom: Sisäluokat ja private-määritellyt asiat saattavat aiheuttaa ongelmia! (Esimerkkiluokka X.java jumiuttaa kääntäjän (versio 1.1.3!)) (Sun ilmoitti minulle 26.1.1998, että kyseinen virhe on korjattu JDK:n versiossa 1.1.5.)


Takaisin Ohjelmoinnin harjoitustyön pääsivulle.