Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

13 Ohjelmointitekniikkaa: gui ja tapahtumaohjattu ohjelma

(Muutettu viimeksi 8.12.2011, sivu perustettu 29.11.2010. Arto Wikla)

Tämä luku esittelee eräitä ideoita ja ohjelmointitapoja graafisen käyttöliittymän tarjoavien ohjelmien toteuttamisessa ("gui" = "GUI" = "graphical user interface").

Graafinen sovellus

Sovellus on main-metodilla käynnistettävä Java-ohjelma, sovelma on www-sivulla toimiva ohjelma. Graafiseen käyttöliittymään perustuvan sovelluksen päämetodin ainoa tehtävä on konstruktoria kutsuen luoda ohjelman ikkuna ja säätää ikkunan asetuksia.

Konstruktorin tehtävänä on luoda varsinainen näkymä käyttöliittymäelementteineen ja niihin liittyvine toiminnallisuuksineen.

Esimerkki:

import javax.swing.*;

public class Hoi extends JFrame {

  // käyttöliittymäkenttä:
  private JTextField hoi;
  
  // konstruktori:
  public Hoi() {
    hoi = new JTextField("Hoi maailma!"); // käyttöliittymäolio
    add(hoi); // lisätään olio luokan näkymään
  }

  public static void main(String args[]) {
    Hoi ikkuna = new Hoi();  // luodaan Hoi-olio
    ikkuna.setTitle("Hoi");  // yläpalkkiin otsikko
    ikkuna.pack();           // kootaan kamat yhteen
    ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // osaa loppua
    ikkuna.setVisible(true); // olio näkyviin
  }
}

Käynnistetään ohjelma tavalliseen tapaan komentoriviltä:

$ java Hoi

Kuvaruudulle ilmestyy pienen pieni ohjelmaikkuna:

kuva Hoi.png

Vaikka tämä ohjelma ei mitään palvelua tarjoakaan, se on silti ihan kunnollinen pikku ikkuna, joka osaa samat taidot kuin mahtavimmatkin ikkunat: sitä voi siirrellä, sen voi minimoida alapalkkiin tai suurentaa kuvaruudun kokoiseksi, venyttää ja vanuttaa reunoista ja nurkista, sulkea, ... tehdä yleensäkin kaikkea mitä kunnon ikkunalta sopii odottaa.

Mistä ihmeestä se on taitonsa perinyt? Hmm! Hetkinen "perinyt"... Jokin lamppu taitaa syttyä...

Graafista käyttöliittymää käyttävän sovelluksen luokka on luokan JFrame aliluokka. JFrame määrittelee ikkunan, jolla on reunat ja otsikkopalkki. Se tarjoaa yliluokkineen välineet mm. ikkunoiden käsittelyyn.

Käyttöliittymäelementtejä ja asemointia

Käyttöliittymiä rakennetaan monenlaisista elementeistä, tekstikentistä, nappuloista (virallisesti kai "painikkeista"?), tekstialueista, alasvetovalikoista, ponnahdusikkunoista, ... Javastakin tällaisia löytyy lievästi sanottuna "joka lähtöön".

Asemointi tarkoittaa visuaalisten komponenttien asettelua toistensa suhteen. Lehtien yhteydessä puhutaan "taittamisesta". Fingelskaksi tavataan puhua "lay-outista".

Graafisen käyttöliittymän lay-outin ohjelmointi ei ole kovin helppoa ja vaivatonta, koska ohjelman tulisi olla käytettävissä erilaisilla laitteistoilla; resoluutiot vaihtelevat, "teemat" eli ulkoasutyylit vaihtelevat, jne.

Javassa on tarjolla useita asemointitapoja. Tässä luvussa tutustutaan vain kolmeen:


Esimerkki FlowLayout-asemoinnista ja elementeistä JTextField, JButton ja JTextArea.

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

public class ElemFlow extends JFrame {

  // käyttöliittymäkentät:

  private JTextField kentta;
  private JButton nappula;
  private JTextArea alue;
  private JLabel otsikko;
  private JTextField viestikentta;

  public ElemFlow() { // konstruktori!

    // luodaan käyttöliittymäelementit, oliot:

    kentta = new JTextField("Kentta");
    nappula = new JButton("Nappula");
    alue = new JTextArea(3,7);
    otsikko = new JLabel("Otsikko");

    viestikentta = new JTextField("Viestikentta");
    viestikentta.setEditable(false);  // muuttamattomaksi!

    // valitaan asemointi:

    setLayout(new FlowLayout());

    // lisätään kentät näkymään:

    add(kentta);
    add(nappula);
    add(alue);
    add(otsikko);
    add(viestikentta);
  }

  public static void main(String args[]) {
    ElemFlow ikkuna = new ElemFlow();
    ikkuna.setTitle("ElemFlow");
    ikkuna.pack();
    ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    ikkuna.setVisible(true);
  }
}

Ja tällaiselta FlowLayout voi näyttää:

kuva ElemFlow.png kuva ElemFlow2.png

Tätä ikkunaa kannattaa kokeilla venyttää ja vanuttaa!


Esimerkki GrigLayout-asemoinnista:

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

public class ElemGrid extends JFrame {

  // käyttöliittymäkentät:

  private JTextField kentta;
  private JButton nappula;
  private JTextArea alue;
  private JLabel otsikko;
  private JTextField viestikentta;

  public ElemGrid() { // konstruktori!

    // luodaan käyttöliittymäelementit, oliot:

    kentta = new JTextField("Kentta");
    nappula = new JButton("Nappula");
    alue = new JTextArea(3,7);
    otsikko = new JLabel("Otsikko");

    viestikentta = new JTextField("Viestikentta");
    viestikentta.setEditable(false);  // muuttamattomaksi!

    // valitaan asemointi:

    setLayout(new GridLayout(3,2));

    // lisätään kentät näkymään:

    add(kentta);
    add(nappula);
    add(alue);
    add(otsikko);
    add(viestikentta);
  }

  public static void main(String args[]) {
    ElemGrid ikkuna = new ElemGrid();
    ikkuna.setTitle("ElemGrid");
    ikkuna.pack();
    ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    ikkuna.setVisible(true);
  }
}

Ja tältä GrigLayout näyttää (huomaa, että matriisin alkio 3,2 on jäänyt tyhjäksi):

kuva ElemGrid.png


Esimerkki BorderLayout-asemoinnista:

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

public class ElemBorder extends JFrame {

  // käyttöliittymäkentät:

  private JTextField kentta;
  private JButton nappula;
  private JTextArea alue;
  private JLabel otsikko;
  private JTextField viestikentta;

  public ElemBorder() { // konstruktori!

    // luodaan käyttöliittymäelementit, oliot:

    kentta = new JTextField("Kentta");
    nappula = new JButton("Nappula");
    alue = new JTextArea(3,7);
    otsikko = new JLabel("Otsikko");

    viestikentta = new JTextField("Viestikentta");
    viestikentta.setEditable(false);  // muuttamattomaksi!

    // valitaan asemointi:

    setLayout(new BorderLayout());

    // lisätään kentät näkymään:

    add("North", kentta);
    add("East", nappula);
    add("Center", alue);
    add("South", otsikko);
    add("West", viestikentta);
  }

  public static void main(String args[]) {
    ElemBorder ikkuna = new ElemBorder();
    ikkuna.setTitle("ElemBorder");
    ikkuna.pack();
    ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    ikkuna.setVisible(true);
  }
}

Ja tältä BorderLayout näyttää:

kuva ElemBorder.png

Panelointia

Sovelluksen ikkuna voidaan rakentaa "ali-ikkunoista", paneeleista, joilla jokaisella voi olla oma asemointitapansa. Ja koko ikkunalla omansa. Ja toki paneeleilla voi olla omia alipaneeleitaan... jne.

Esimerkki paneelien ja muutamien layout-asetusten käytöstä:

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;

import javax.swing.*;

public class Paneeleja extends JFrame {

  private JButton b111 = new JButton("Nappula 111");
  private JTextField t112 = new JTextField("Kentta 112");
  private JLabel l121 = new JLabel("Teksti 121");
  private JButton b122 = new JButton("Nappula 122");

  private JButton b21 = new JButton("Nappula 21");
  private JTextField t22 = new JTextField("Kentta 22");
  private JButton b23 = new JButton("Nappula 23");

  private JButton b31 = new JButton("31");
  private JButton b32 = new JButton("32");
  private JButton b33 = new JButton("33");
  private JButton b34 = new JButton("34");
  private JButton b35 = new JButton("35");
  private JButton b36 = new JButton("36");
  private JButton b37 = new JButton("37");
  private JButton b38 = new JButton("38");

  private JButton b41 = new JButton("41");
  private JButton b42 = new JButton("42");

  public Paneeleja() {

    JPanel p11 = new JPanel(new BorderLayout());
    p11.add("North", b111);
    p11.add("South", t112);

    JPanel p12 = new JPanel(new BorderLayout());
    p12.add("North", l121);
    p12.add("South", b122);

    JPanel p1 = new JPanel(new FlowLayout());
    p1.add(p11); p1.add(p12);

    JPanel p2 = new JPanel(new FlowLayout());
    p2.add(b21); p2.add(t22); p2.add(b23);

    JPanel p3 = new JPanel(new GridLayout(2,4));
    p3.add(b31); p3.add(b32); p3.add(b33); p3.add(b34);
    p3.add(b35); p3.add(b36); p3.add(b37); p3.add(b38);

    JPanel pp1 = new JPanel(new BorderLayout());
    pp1.add("North", p1);
    pp1.add("Center", p2);
    pp1.add("South", p3);

    JPanel pp2 = new JPanel(new GridLayout(2,1));
    pp2.add(b41); pp2.add(b42);

    this.setLayout(new BorderLayout());
    this.add("Center", pp1);
    this.add("East", pp2);
  }

  public static void main(String args[]) {
    Paneeleja ikkuna = new Paneeleja();
    ikkuna.setTitle("Paneeleja");
    ikkuna.pack();
    ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    ikkuna.setVisible(true);
  }
}

Ja tältä se sitten näyttää:

kuva Paneeleja.png

Tapahtumat ja reagointi

Kurssilla aiemmin laaditut ohjelmat ovat perustuneet ajatukseen, että pääohjelma tai sen kutsumat aliohjelmat ovat aina lopulta päättäneet ohjelman osien suoritusjärjestyksestä. Yksinkertaisimmillaan ohjelma on pyytänyt syötteitä ja sitten vain laskenut ja kirjoittanut tulokset. Komentotulkkilogiikka on ollut valmiina toteuttamaan yhden komennon kerrallaan. Ohjelma on voinut myös kirjoittaa näkyville valikon, josta käyttäjä on vaikkapa numeron perusteella valinnut suoritettavan operaation.

Graafiset käyttöliittymät ohjelmoidaan eri tavalla; ne ovat tapahtumaohjattuja eli tapahtumapohjaisia, "event driven". Ja tapahtumaohjatussa toimintalogiikassa kaikki on toisin...

Käyttöliittymäelementteihin liitetään tapahtumankuuntelijoita (actionlistener), jotka ovat algoritmeja, joita kutsutaan, kun ohjelman ulkopuolinen järjestelmä havaitsee elementtiin kohdistuvan tapahtuman: nappulan painalluksen, enter-painalluksen tekstikentässä, ...

Javassa tapahtumankuuntelija kytketään käyttöliittymäelementtiin aksessorilla addActionListener:

kentta.addActionListener(...);

Parametriksi – loppujen lopuksi – annetaan algoritmi, joka toteuttaa kyseessä olevaan käyttöliittymäelementtiin liittyvän toiminnon, reaktion tapahtumaan.

Java-kielessä (valitettavasti!) algoritmeja ei sellaisenaan voi välittää parametrina. Niinpä ohjelmoija joutuu himpun verran "Java-temppuilemaan": addActionListener-aksessorin muodollisen parametrin tyyppi on rajapintaluokka ActionListener. Se vaatii toteuttamaan ainostaan metodin public void actionPerformed(ActionEvent tapahtuma).

Ohjelmoija voi toki tuttuun tapaan laatia luokan, joka toteuttaa tuon vaaditun rajapintaluokan. Mutta seurauksena saattaa olla ylistrukturointia: jokainen pikku reaktioalgoritmi vaatii oman luokkansa.

Yksi luonteva tapa toteuttaa näitä reagointialgoritmeja on käyttää Javan ns. nimettömiä sisäluokkia: addActionListener-aksessorin todelliseksi parametriksi voidaan kirjoittaa ilmaus, jossa ensin kerrotaan luotavan "rajapintaluokan ilmentymä": new ActionListener(). Sitten annetaan luokkamäärittely tavalliseen tapaan aaltosulkeissa. Ja tuon määrittelyn on sisällettävä rajapintaluokan vaatiman metodin toteutus. Eikä siellä useinkaan muuta olekaan.

...
import java.awt.event.*;
...
private JElemTyyppi kentta;
...
kentta = new JElemTyyppi();
...
kentta.addActionListener(
  new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          // Tänne kirjoitetaan reagointialgoritmi tilanteeseen, 
          // jossa kentta-muuttujan arvona olevalle elementtioliolle
          // tapahtuu jotakin. Suoritusaikainen järjestelmä kutsuu
          // actionPerformed-metodia ja antaa tapahtumaolion avulla
          // tietoa tapahtuneesta.
          //
          // HUOM: Ympäröivän luokan muuttujat näkyvät tänne, mutta niiden
          //       arvoja ei voi muuttaa - muuttujien on oltava final.
        }
      }
    );
...

Ei MVC vaan mikä?

Klassinen ohjelmarakenne – ohjelma-arkkitehtuuri jos niin halutaan sanoa – graafisen käyttöliittymän ohjelmien laatimisessa on nimeltään Model-View-Controller (MVC) (malli-näkymä-ohjain). Siinä ideana on erottaa mahdollisimman hyvin toisistaan itse laskennan logiikka eli sovelluksen todellinen viisaus (malli), näkymän rakentaminen ja ylläpito tapahtumien havaitsemisineen (näkymä) ja lopulta kaiken kontrollointi: huolenpito siitä, että syötteet ja tulosteet kulkevat näytettäviksi halutulla tavalla ja että syötteet viedään laskennan logiikalle jalostettaviksi kunnollisiksi tulosteiksi (ohjain).

Usein ainakin pienehköissä Java-ohjelmissa näkymän ja ohjaimen yhdistäminen on järkevää, mutta malli on syytä erottaa omaksi luokakseen.

Laaditaan esimerkkinä graafinen kuulaskurisovellus, joka tarjoaa kuukauden numeron näyttämisen ja kuukaudesta seuraavaan etenemisen sekä nollauksen ja lopetuksen. Itse laskennan arvatenkin monimutkainen toteutus annetaan vanhan tutun Kuulaskurin huoleksi. Siitä gui-ohjelmamme ei tiedä eikä ymmärrä mitään...

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

public class Kuu extends JFrame {

  // kenttä koneelle, joka osaa kuukausien käsittelyn monimutkaisen logiikan:

  private Kuulaskuri kuuKone;

  // käyttöliittymäkentät:

  private JTextField kuuikkuna;
  private JButton    kuunappula;
  private JButton    kuunnollaus;
  private JButton    lopetus;

  public Kuu() { // konstruktori!

    // luodaan monimutkaisen laskentalogiikan osaava kone:

    kuuKone = new Kuulaskuri();

    // käyttöliittymäoliot alkutilaansa:

    kuuikkuna  = new JTextField();
    kuuikkuna.setEditable(false);  // kirjoituskielto käyttäjälle
    kuuikkuna.setText("           " + kuuKone.moneskoKuu());

    kuunappula  = new JButton("Seuraava!");
    kuunnollaus = new JButton(" Alkuun!");
    lopetus     = new JButton(" Lopetus!");

    // tapahtumankuuntelijoiden asetukset:

    kuunappula.addActionListener(   // kuukausi etenee
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          kuuKone.seuraavaKuu();
          kuuikkuna.setText("           " + kuuKone.moneskoKuu());
          if (kuuKone.moneskoKuu()==1)
            JOptionPane.showMessageDialog(null, "Hyvää uutta vuotta!"); // ponnahdus!
        }
      }
    );

    kuunnollaus.addActionListener(  // aloitetaan puhtaalta pöydältä uudella laskurilla
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          kuuKone = new Kuulaskuri();
          kuuikkuna.setText("           " + kuuKone.moneskoKuu());
        }
      }
    );

    lopetus.addActionListener(   // varmistetaan lopetushalu ponnahdusikkunalla
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          if (JOptionPane.showConfirmDialog(null, "Lopetetaanko todella?")==0) // ponnahdus!
            System.exit(0);
        }
      }
    );

    // valitaan asemointityyli:

     setLayout(new GridLayout(4,1));  // rivejä - sarakkeita

    // käyttöliittymäelementit näkyviin valitussa lay-outissa:

    add(kuuikkuna);
    add(kuunappula);
    add(kuunnollaus);
    add(lopetus);
  }

  public static void main(String args[]) {
    Kuu ikkuna = new Kuu();
    ikkuna.setTitle("Kuu");
    ikkuna.pack();
    ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // osaa loppua
    ikkuna.setVisible(true); // olio näkyviin
  }
}

Ja tällaiselta Kuu sitten näyttää ("näin piirretään kuu"...):

kuva Kuu.png

Huomioita:

Piirustusta alustalle

Piirustusalusta voidaan toteuttaa luokan JPanel aliluokkana. Seuraavissa esimerkeissä aliluokka on kirjoitettu samaan käännösyksikköön varsinaisen ohjelman luokan kanssa. Tämä ei tietenkään ole välttämätöntä.

Oma piirustusalusta perii yliluokaltaan mm. metodin protected void paintComponent(Graphics g). JPanel on puolestaan itse perinyt tuon metodin omalta yliluokaltaan JComponent. Ohjelman laatija syrjäyttää (overrides) tämän perityn metodin sellaisella, joka piirtää komponenttiin halutun sisällön.

Järjestelmä kutsuu paintComponent(Graphics g)-metodia aina, kun näkymä pitää piirtää: ohjelman suorituksen alussa, kun näkymä on ollut peitettynä, kun se palautetaan kuvakkeesta ikkunaksi, ... Kutsuva järjestelmä antaa metodille parametrina Graphics-luokan ilmentymän, joka juuri on ohjelman ikkuna. (Kyseessä on itse asiassa Graphics-luokan jonkin aliluokan ilmentymä, koska Graphics itse on abstrakti!)

Luokassa Graphics on määritelty joukko metodeita sovelman ikkunassa näytettävän kuvan muodostamiseen ja käsittelyyn:

Ikkunan koordinaatit ilmaistaan kuvapisteinä (ns. pikseleinä) siten, että ikkunan vasemman yläkulman koordinaatit ovat (0,0), yläkulman alapuolinen pikseli on (0,1), yläkulman viereinen pikseli on (1,0), jne.

Huom: Piirrellä voidaan itse asiassa melkein minkä vain komponentin pinnalle! JComponent-luokalle luetellaan "tunnettuina aliluokkina" seuraavat: AbstractButton, BasicInternalFrameTitlePane, Box, Box.Filler, JColorChooser, JComboBox, JFileChooser, JInternalFrame, JInternalFrame.JDesktopIcon, JLabel, JLayeredPane, JList, JMenuBar, JOptionPane, JPanel, JPopupMenu, JProgressBar, JRootPane, JScrollBar, JScrollPane, JSeparator, JSlider, JSpinner, JSplitPane, JTabbedPane, JTable, JTableHeader, JTextComponent, JToolBar, JToolTip, JTree, JViewport. Näillä luokillakin on aliluokkia. Esimerkiksi AbstractButton-luokan JButton-aliluokan ilmentymät voivat joutua piirtelyn kohteeksi!

Tekstien kirjoittamista piirustusalustalle

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

public class Kirjoittelua extends JFrame {

  public Kirjoittelua() {
    this.add(new KirjoitusPaneeli());
  }

  public static void main(String[] args) {
        Kirjoittelua ikkuna = new Kirjoittelua();
        ikkuna.setTitle("Kirjoittelua");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.setSize(200, 100);
        ikkuna.setVisible(true);
    }
}

class KirjoitusPaneeli extends JPanel {
  public void paintComponent(Graphics g) {

    g.drawString("Arkipäiväistä", 10, 15);

    Font kirjasin = new Font("TimesRoman", Font.BOLD+Font.ITALIC, 14);
    g.setFont(kirjasin);
    g.drawString("Juhlavaa", 20, 35);

  }
}

Ja tältä tuo näyttää:

kuva Kirjoittelua.png

Piirtelyä ikkunaan

Graphics-luokassa on määritelty joukko piirtämisvälineitä. Metodit on määritelty abstrakteina. Graphicsin aliluokat siis itse asiassa toteuttavat nuo välineet omien vaatimustensa mukaisina.

Esimerkki muutamien hahmojen piirtämisestä:

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

public class Hahmoja extends JFrame {

  public Hahmoja() {
    add(new HahmoPaneeli());
  }

  public static void main(String[] args) {
        Hahmoja ikkuna = new Hahmoja();
        ikkuna.setTitle("Hahmoja");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.setSize(300, 200);
        ikkuna.setVisible(true);
    }
}

class HahmoPaneeli extends JPanel {

  public void paintComponent(Graphics g) {

     g.drawLine(25,35, 50,100);     // viiva pisteestä pisteeseen
     g.drawString("Hahmoja",10,20); // teksti
     g.drawRect(200,50, 20,40);     // nelikulmio, vasen nurkka, leveys, korkeus
     g.drawOval(140,120, 40,20);    // soikio, keskipiste, leveys, korkeus
     g.fillOval(100,50, 25,25);     // täytetty soikio
  }
}

Syntyy näkymä:

kuva Hahmoja.png

Seuraava esimerkkiohjelma arpoo janan paikan

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

public class Arpaviiva extends JFrame {

  public Arpaviiva() {
    add(new ArpaPaneeli());
  }

  public static void main(String[] args) {
        Arpaviiva ikkuna = new Arpaviiva();
        ikkuna.setTitle("Arpaviiva");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.setSize(300, 200);
        ikkuna.setVisible(true);
    }
}

class ArpaPaneeli extends JPanel {

  public void paintComponent(Graphics g) {

     int alkux,alkuy, loppux, loppuy;

     alkux  = (int)(Math.random()*300);
     alkuy  = (int)(Math.random()*200);
     loppux = (int)(Math.random()*300);
     loppuy = (int)(Math.random()*200);

     g.drawLine(alkux,alkuy,loppux,loppuy);
     g.drawString("("+alkux+","+alkuy+"),("+loppux+","+loppuy+")",1,12);
  }
}

Ohjelma voi alussa näyttää vaikka tältä:

kuva Arpaviiva.png

Luokassa Color on värienkäsittelyvälineitä. Siellä on myös vakiot mm. seuraaville väreille: black, blue, cyan, darkGray, gray, green, lightGray, magenta, orange, pink, red, white ja yellow. Nämä voi kirjoittaa myös isoin kirjaimin.

Värejä voi määritellä myös ns. RGB-lukukolmikkoina, jotka annetaan Color-konstruktorille. Valkoinen on Color(255,255,255), musta Color(0,0,0), puhdas punainen Color(255,0,0), ...

Esimerkki: Kokeillaan RGB-määriteltyjä värejä. Käydään läpi R- ja G-arvoja ja B-komponentti arvotaan joka maalauskerralla.

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

public class OmatVarit extends JFrame {

  public OmatVarit() {
    add(new VariPaneeli());
  }

  public static void main(String[] args) {
        OmatVarit ikkuna = new OmatVarit();
        ikkuna.setTitle("OmatVarit");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.setSize(285, 310);
        ikkuna.setVisible(true);
    }
}

class VariPaneeli extends JPanel {

  public void paintComponent(Graphics kuva) {

     setBackground(Color.white);

     int r,g,b; // RGB värikolmikko

     b = (int)(Math.random()*256);

     for (r=0; r<256; r+=3)
       for (g=0; g<256; g+=3) {
         Color vari = new Color(255-r,255-g,b);
         kuva.setColor(vari);
         kuva.fillRect(10+r,10+g, 3,3);
       }
  }
}

Näkymä voi alussa olla vaikkapa tällainen:

kuva OmatVarit.png

Nappuloita ja kuvia

Kuvia ja muita käyttöliittymäelementtejä voidaan toki yhdistellä.

Esimerkissä tuotetaan uusi maalaus napin painalluksella:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.Graphics;
import java.awt.Color;

public class Maalaus extends JFrame {

  private JButton nappula = new JButton("Uusi maalaus!");
  private VariPaneeli kuva = new VariPaneeli();

  public Maalaus() {

    setLayout(new BorderLayout());
    add("Center", kuva);
    add("South", nappula);

    nappula.addActionListener(
       new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          kuva.repaint();
        }
      }
    );
  }

  public static void main(String[] args) {
        Maalaus ikkuna = new Maalaus();
        ikkuna.setTitle("Art or Not");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.setSize(285, 330);
        ikkuna.setVisible(true);
    }
}

class VariPaneeli extends JPanel {
  public void paintComponent(Graphics kuva) {

    setBackground(Color.white);

    int r,g,b; // RGB värikolmikko

    b = (int)(Math.random()*256);

    for (r=0; r<256; r+=3)
      for (g=0; g<256; g+=3) {
        Color vari = new Color(255-r,255-g,b);
        kuva.setColor(vari);
        kuva.fillRect(10+r,10+g, 3,3);
     }
  }
}

Ohjelma voi alussa näyttää vaikka tältä:

kuva Maalaus.png

Ja "napin painalluksella" saa yhä uusia erilaisia maalauksia.

Aina sattuu ja tapahtuu

Laaditaan väline, jolla voi kokeilla sovelluksen reagointeja rajapintaluokkien ComponentListener, MouseMotionListener, MouseWheelListener, MouseListener, KeyListener ja FocusListener määrittelemiin tapahtumiin.

Koska luokka Reagointeja lupaa toteuttaa kaikki mainitut rajapintaluokat, sen on annettava toteutus niiden kaikille metodeille! Metodit vain tulostavat ilmoituksen itsestään siihen ikkunaan, josta sovellus on käynnistetty - tuttuun tapaan metodilla System.out.println(...)!

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

public class Reagointeja
               extends JFrame
               implements ComponentListener, MouseMotionListener, 
                          MouseListener, MouseWheelListener,
                          KeyListener, FocusListener {

   private TextArea alue =
     new TextArea ("\n    Reagoiva alue:\n\n" + 
                   "   Kokeile erilaisia\n" +
                   "   tapahtumia ja seuraa\n" +
                   "   systeemin ikkunaa!",
                   10, 20, TextArea.SCROLLBARS_NONE);

   private int laskuri = 0;
   private Label laskuriOtsikko = new Label("", Label.CENTER);

   public Reagointeja() {

      alue.setEditable(false);

      add ("Center", alue);
      add ("South", laskuriOtsikko);

      addComponentListener(this);

      alue.addFocusListener(this);
      alue.addKeyListener(this);
      alue.addMouseListener(this);
      alue.addMouseMotionListener(this);
      alue.addMouseWheelListener(this);
   }

   private void kirjaaTapahtuma(AWTEvent tapahtuma) {
      laskuri++;
      laskuriOtsikko.setText("Tapahtumia: " + laskuri + " kpl");
      System.out.println(tapahtuma.toString().substring(15));
      // ei tulosteta alkuosaa "java.awt.event."!
   }

   public void componentMoved(ComponentEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void componentHidden(ComponentEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void componentResized(ComponentEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   } 
   public void componentShown(ComponentEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseDragged(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseMoved(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mousePressed(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseReleased(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseEntered(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseExited(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseClicked(MouseEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void keyPressed(KeyEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void keyReleased(KeyEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void keyTyped(KeyEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void focusGained(FocusEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void focusLost(FocusEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }
   public void mouseWheelMoved(MouseWheelEvent tapahtuma) {
      kirjaaTapahtuma(tapahtuma);
   }

   public static void main(String args[]) {
        Reagointeja ikkuna = new Reagointeja();
        ikkuna.setTitle("Tapahtuu ...");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.setSize(200, 200);
        ikkuna.setVisible(true);
    }
}

Ja tässä tulee todellinen hyötyohjelma!

import javax.swing.*;
import java.awt.event.*;
import java.awt.font.*;  // vain fonttia varten
import java.awt.*;       //        "

public class HiiriTuliJaMeni extends JFrame
                                implements MouseListener {

  private JTextArea alue = new JTextArea (1, 11);
  private int puremia = 0;

  public HiiriTuliJaMeni() {

    alue.setEditable(false);
    add ("Center", alue);

    alue.setFont(new Font("Serif", Font.BOLD, 20));
    alue.setText(" Hiirtä ei ole näkynyt.");

    alue.addMouseListener(this);  // luokka ITSE toteuttaa
                                  // kuuntelijan
  }

  public void mouseEntered(MouseEvent tapahtuma) {
    alue.setText("      Hiiri tuli!");
  }
   public void mouseExited(MouseEvent tapahtuma) {
    alue.setText("      Hiiri meni!");
  }

  public void mouseClicked(MouseEvent tapahtuma) {
    ++puremia;
    alue.setText(" Hiiri puri! ("+puremia+". kerta)"); 
  }

  // KAIKKI luvatut metodit on toteutettava (edes tyhjinä):

  public void mousePressed(MouseEvent tapahtuma)  { }
  public void mouseReleased(MouseEvent tapahtuma) { }


 public static void main(String[] args) {
        HiiriTuliJaMeni ikkuna = new HiiriTuliJaMeni();
        ikkuna.setTitle("Hiirielämää");
        ikkuna.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ikkuna.pack();
        ikkuna.setVisible(true);
    }
}

Sovelluksesta sovelmaan

Sovelmat (applet) toteutetaan hyvin samaan tapaan kuin graafiset sovellukset. Konstruktorin korvaa init()-metodi, jota kutsuen www-selain luo sovelman graafisen näkymän. Pääohjelmaa ei tarvita - selain hoitaa sen tehtävät. Sovelman elinkaareen liittyy muutama muukin metodi, joilla voi esimerkiksi käynnistää tai keskeyttää animoinnin yms. Niihin kuitenkaan ei tällä kurssilla ole aikaa puuttua.

Graafisen sovelluksen muuntaminen sovelmaksi on tehtävissä lähes algoritmisesti:

Edellä tavattu Kuu-sovellus voidaan muuttaa KuuSovelma-sovelmaksi seuraavaan tapaan:

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

public class KuuSovelma extends JApplet {  // JFrame ---> JApplet

  // kenttä koneelle, joka osaa kuukausien käsittelyn monimutkaisen logiikan:

  private Kuulaskuri kuuKone;

  // käyttöliittymäkentät:

  private JTextField kuuikkuna;
  private JButton    kuunappula;
  private JButton    kuunnollaus;

  public void init() {   // INIT KORVAA KONSTRUKTORIN!

    // luodaan monimutkaisen laskentalogiikan osaava kone:

    kuuKone = new Kuulaskuri();

    // käyttöliittymäoliot alkutilaansa:

    kuuikkuna  = new JTextField();
    kuuikkuna.setEditable(false);  // kirjoituskielto käyttäjälle
    kuuikkuna.setText("           " + kuuKone.moneskoKuu());

    kuunappula  = new JButton("Seuraava!");
    kuunnollaus = new JButton(" Alkuun!");
//    lopetus     = new JButton(" Lopetus!");  LOPETTAMINEN POIS!

    // tapahtumankuuntelijoiden asetukset:

    kuunappula.addActionListener(   // kuukausi etenee
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          kuuKone.seuraavaKuu();
          kuuikkuna.setText("           " + kuuKone.moneskoKuu());
          if (kuuKone.moneskoKuu()==1)
            JOptionPane.showMessageDialog(null, "Hyvää uutta vuotta!");
        }
      }
    );

    kuunnollaus.addActionListener(  // aloitetaan puhtaalta pöydältä uudella laskurilla
      new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          kuuKone = new Kuulaskuri();
          kuuikkuna.setText("           " + kuuKone.moneskoKuu());
        }
      }
    );

    // valitaan asemointityyli:

     setLayout(new GridLayout(4,1));  // rivejä - sarakkeita

    // käyttöliittymäelementit näkyviin valitussa lay-outissa:

    add(kuuikkuna);
    add(kuunappula);
    add(kuunnollaus);

  }

//  public static void main(String args[]) {  PÄÄOHJELMA POIS!

}

Sovelma liitetään www-sivulle erityisellä ilmauksella ("tägillä"):

<APPLET CODE="KuuSovelma.class" WIDTH=200 HEIGHT=100>
Selain ei ymmärrä Javaa!
</APPLET>

Vielä on pidettävä huoli, että kaikille sovelman käyttämille luokille, class-tiedostoille, asetetaan lukuoikeus koko maailmalle. Kuusovelma tarvitsee useita luokkia:

-rw-r--r-- 1 wikla grpb  419 2010-12-01 09:28 Kuulaskuri.class
-rw-r--r-- 1 wikla grpb 1180 2010-12-01 09:53 KuuSovelma$1.class
-rw-r--r-- 1 wikla grpb 1045 2010-12-01 09:53 KuuSovelma$2.class
-rw-r--r-- 1 wikla grpb  701 2010-12-01 09:28 KuuSovelma$3.class
-rw-r--r-- 1 wikla grpb 1676 2010-12-01 09:53 KuuSovelma.class

(Nuo $-merkin sisältävät class-tiedostot ovat syntyneet nimettömistä sisäluokista.)

Ja tällainen se Kuusovelma sitten on:

Selain ei ymmärrä Javaa!

Vastaavaan tapaan Maalaus voidaan muokata sovelmaksi:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.Graphics;
import java.awt.Color;

public class MaalausSovelma extends JApplet {

  private JButton nappula = new JButton("Uusi maalaus!");
  private VariPaneeli kuva = new VariPaneeli();

  public void init() {

    setLayout(new BorderLayout());
    add("Center", kuva);
    add("South", nappula);

    nappula.addActionListener(
       new ActionListener () {
        public void actionPerformed(ActionEvent tapahtuma) {
          kuva.repaint();
        }
      }
    );
  }
}


class VariPaneeli extends JPanel {
  public void paintComponent(Graphics kuva) {

    setBackground(Color.white);

    int r,g,b; // RGB värikolmikko

    b = (int)(Math.random()*256);

    for (r=0; r<256; r+=3)
      for (g=0; g<256; g+=3) {
        Color vari = new Color(255-r,255-g,b);
        kuva.setColor(vari);
        kuva.fillRect(10+r,10+g, 3,3);
     }
  }
}

Selain ei ymmärrä Javaa!

Ja loppuhuipennukseksi HiiriTuliJaMeni sovelmaksi:

import javax.swing.*;
import java.awt.event.*;
import java.awt.font.*;  // vain fonttia varten
import java.awt.*;       //        "

public class HiiriTuliJaMeniSovelma extends JApplet
                                implements MouseListener {

  private JTextArea alue = new JTextArea (1, 11);
  private int puremia = 0;

    public void init() {

    alue.setEditable(false);
    add ("Center", alue);

    alue.setFont(new Font("Serif", Font.BOLD, 20));
    alue.setText(" Hiirtä ei ole näkynyt.");

    alue.addMouseListener(this);  // luokka ITSE toteuttaa
                                  // kuuntelijan
  }

  public void mouseEntered(MouseEvent tapahtuma) {
    alue.setText("      Hiiri tuli!");
  }
   public void mouseExited(MouseEvent tapahtuma) {
    alue.setText("      Hiiri meni!");
  }

  public void mouseClicked(MouseEvent tapahtuma) {
    ++puremia;
    alue.setText(" Hiiri puri! ("+puremia+". kerta)");
  }

  // KAIKKI luvatut metodit on toteutettava (edes tyhjinä):

  public void mousePressed(MouseEvent tapahtuma)  { }
  public void mouseReleased(MouseEvent tapahtuma) { }
}

Selain ei ymmärrä Javaa!