javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

4 Komponentbaseret programmering


4.1 Genbrugelige komponenter 64

4.1.1 Javabønner 64

4.1.2 Eksempel: Bønnen TextField 64

4.1.3 Bruge en javabønne 65

4.2 At definere javabønner 67

4.2.1 En meget simpel bønne: GentagTekst 67

4.2.2 Brug af bønner fra et udviklingsværktøj 68

4.2.3 Størrelsen af en grafisk komponent 68

4.2.4 Skelne mellem udviklings- og i kørselsfasen 69

4.3 Karakteristika ved javabønner 70

4.3.1 Bønner skal have en parameterløs konstruktør 70

4.3.2 Bønner kan have egenskaber 70

4.3.3 Bønner kan have tilknyttet ekstra information 71

4.3.4 Bønner bør være afgrænsede og uafhængige 71

4.3.5 Bønner kan understøtte hændelses-lyttere 71

4.4 Ekstra eksempler 73

4.4.1 Rystetekst 73

4.4.2 Rulletekst 74

4.4.3 Simpel kryptering 75

4.4.4 Eksempel på brug af bønnerne i et værktøj 76

4.5 Opgaver 78

4.6 Løsninger 79

4.6.1 Grafisk komponent: GentagTekst 79

4.6.2 Grafisk komponent: Billede 79

4.6.3 Webserver-komponent 80

4.7 Avanceret 81

4.7.1 En komponent til at tegne kurver 81

4.7.2 Repræsentation af funktioner 84

4.7.3 Fortolkning af strenge til funktioner 86

4.7.4 Layout-manageres virkemåde 87

4.7.5 Øvelse: Samspil med layout-manager 88

4.7.6 Opgave: Adresseindtastningskomponent 88

4.7.7 Løsning: Adresseindtastningskomponent 89

Eksemplet i afsnit 4.7.3 forudsætter kapitel 3, Rekursion.

4.1 Genbrugelige komponenter

Komponenter er programmørens byggeklodser: De kan bruges igen og igen i mange sammenhænge og sættes sammen på alle mulige måder.

En bred definition på 'komponent' kunne være 'afgrænset programdel, der kan genanvendes i flere sammenhænge'. Med denne definition er der dog ret meget, der er komponenter:

For alle komponenter gælder, at deres omgivelser - beholderen eller containeren - også er vigtig. En komponent skal bruges på en bestemt måde, og den vil ikke fungere uden de rette omgivelser. F.eks. virker et TextField-objekt ikke, hvis dets paint()-metode ikke kaldes (det sørger java.awt.Container for). En applet skal indlæses i en netlæser/browser, en servlet skal udføres i en webserver. Et Date-objekt skal have kaldt de rette metoder, osv.

4.1.1 Javabønner

En mere snæver definition af begrebet komponent er: 'en afgrænset programdel, der kan genanvendes i flere sammenhænge, som kan bruges i et udviklingsværktøjs palette og som tillader brugeren at manipulere med dets egenskaber ved hjælp af værktøjet'.

Denne definition peger hen på Javabønner (eng.: JavaBeans) som f.eks. de grafiske komponenter Button, Label, TextField, Checkbox, ...

Javabønner har en standardiseret måde at undersøge, hvilke egenskaber (eng.: properties) de har (med get- og set-metoder, se senere), og de fleste udviklingsværktøjer ved derfor, hvordan disse komponenter kan konfigureres.

Der er derfor ingen kode i værktøjet beregnet specielt mod de enkelte javabønner. Nye bønner kan uden videre føjes til udviklingsværktøjets palette. Senere i kapitlet vil vi selv programmere nogle bønner og føje dem til paletten.

4.1.2 Eksempel: Bønnen TextField

Eksempelvis har TextField nogle egenskaber:

text angiver, hvad der står i feltet.

columns angiver, hvor bredt feltet skal være.

editable angiver, om brugeren kan redigere teksten i indtastningsfeltet.

echoChar bruges til felter, der skal skjule det indtastede, typisk adgangskoder.

Egenskab

Type

Sættes med metoden

Aflæses med metoden

text

String

setText(String t)

getText()

editable

boolean

setEditable(boolean rediger)

isEditable()

columns

int

setColumns(int bredde)

getColumns()

echoChar

char

setEchoChar(char tegn)

getEchoChar()

Abonnement på hændelser

Bønnen kan blandt andet sende Action-hændelser. Det betyder at, den har metoderne

  public void addActionListener(ActionListener l)
  public void removeActionListener(ActionListener l)

til at tilføje og fjerne en lytter på denne komponent.

Trykker man retur i indtastningsfeltet, oprettes et hændelses-objekt (af type ActionEvent), og alle lytterne får kaldt metoden actionPerformed() med dette objekt.

4.1.3 Bruge en javabønne

Lad os lave et program, der bruger bønnen TextField, sætter bønnens egenskab text, lytter efter, om brugeren trykker retur i indtastningsfeltet (abonnerer på Action-hændelser fra komponenten) og udskriver indholdet af tekstfeltet, når det sker.

Bruge en javabønne fra et udviklingsværktøj

Genereres koden med et udviklingsværktøj, kommer kildeteksten til at se nogenlunde således ud:

import java.awt.*;
import java.awt.event.*;

public class BenytBoenneMedVaerktoej extends Frame
{
  TextField textFieldNavn = new TextField();      // opret bønnen

  public BenytBoenneMedVaerktoej() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void jbInit() throws Exception {
    textFieldNavn.setText("Jacob");               // sæt egenskaben text

    // anonym indre klasse lytter på hændelser og kalder derpå videre til
    // metoden textFieldNavn_actionPerformed()
    textFieldNavn.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        textFieldNavn_actionPerformed(e);
      }
    });

    textFieldNavn.setBounds(new Rectangle(141, 61, 112, 29));
    this.setLayout(null);
    this.add(textFieldNavn, null);
  }

  void textFieldNavn_actionPerformed(ActionEvent e) {
    String navn = textFieldNavn.getText();        // aflæs egenskaben text
    System.out.println("Navnet er: "+navn);
  }

  public static void main(String[] arg)
  {
    BenytBoenneMedVaerktoej vindue = new BenytBoenneMedVaerktoej();
    vindue.setSize(350,100);                      // sæt vinduets størrelse
    vindue.setVisible(true);                      // åbn vinduet
  }
}

Der er oprettet en konstruktør, der kalder metoden jbInit(). Et udviklingsværktøj definerer gerne en separat metode, hvor den initialiserer komponenterne. I JBuilder og JDeveloper hedder den jbInit(), mens den hedder initComponents() i Netbeans og Sun ONE Studio.

I metoden jbInit() lægger værktøjet koden til at initialisere de grafiske komponenter og sørger for at placere dem korrekt i vinduet. Føjer du din egen kode til denne metode, så sørg for, at det ligner værktøjets egen kode, ellers kan værktøjet have svært ved at opretholde sammenhængen mellem kode og design.

Der er brugt en anonym indre klasse1 til at lytte på hændelser (defineret i parameteren til textFieldNavn.addActionListener()). Når hændelsen sker, kalder lytter-objektet videre i metoden textFieldNavn_actionPerformed().

I princippet kunne der altså lige så godt have stået:

    textFieldNavn.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        String navn = textFieldNavn.getText();
        System.out.println("Navnet er: "+navn);
      }
    });

Bruge en javabønne uden et udviklingsværktøj

Skriver man koden selv uden hjælp fra et udviklingsværktøj, vil man sandsynligvis lægge initialiseringen direkte i konstruktøren og lade klassen selv være en ActionListener i stedet for at definere en indre klasse. Så kommer kildeteksten til at se nogenlunde således ud:

import java.awt.*;
import java.awt.event.*;

public class BenytBoenneSkrevetSelv extends Frame implements ActionListener
{
  TextField textFieldNavn = new TextField();

  public BenytBoenneSkrevetSelv() {
    textFieldNavn.setText("Jacob");

    // klassen selv lytter på hændelser
    textFieldNavn.addActionListener(this);

    textFieldNavn.setBounds(new Rectangle(141, 61, 112, 29));
    this.setLayout(null);
    this.add(textFieldNavn, null);
  }

  public void actionPerformed(ActionEvent e) {
    String navn = textFieldNavn.getText();
    System.out.println("Navnet er: "+navn);
  }

  public static void main(String[] arg) {
    BenytBoenneSkrevetSelv vindue = new BenytBoenneSkrevetSelv();
    vindue.setSize(350,100);
    vindue.setVisible(true);              
  }
}

Vi har ladet vinduet selv implementere ActionListener, hvorfor vi blot skriver

    textFieldNavn.addActionListener(this);

og definerer actionPerformed() selv.

4.2 At definere javabønner

Lad os nu se på, hvordan vi selv definerer komponenter/javabønner.

Når man definerer sine egne javabønner, skal man holde tungen lige i munden og huske at skelne mellem tre roller:

  • Programmøren af bønnen (leverandøren) er den person, der skriver bønnens kode (dvs. koden inde i klassen), men kan ikke antage så meget om, hvordan bønnen vil blive brugt. En bønne skal kunne benyttes helt uden at kende den kode, som leverandøren af bønnen har lavet, og være tilpas generel og genanvendelig til, at en anden programmør kan bruge den. Eksempelvis skulle leverandøren til TextField (en programmør ansat i Sun) kode den, så den ikke afhænger ret meget af sine omgivelser.

  • Programmøren, der anvender bønnen i sit program (klienten). I denne rolle (som er den mest almindelige) gør man brug af en bønne (klasse), som en anden programmør har skrevet (dvs. man skriver kode uden for klassen). Eksempel: At bruge TextField udefra, f.eks. ved i et udviklingsværktøj at føje den til den grafiske brugergrænseflade.

  • Slutbrugeren af programmet behøver ikke, kende noget til, hvordan koden til hverken bønnen eller for den sags skyld resten af programmet ser ud.

4.2.1 En meget simpel bønne: GentagTekst

Herunder er en simpel grafisk bønne, der tegner en tekst tre gange skråt under hinanden.

package vp;
import java.awt.*;
public class GentagTekst extends Component
{
  private String tekstDerSkalVises = "gentag";

  public void setTekst(String t) 
  {
    tekstDerSkalVises = t;
  }

  public String getTekst()
  {
    return tekstDerSkalVises;
  }

  public void paint(Graphics g)
  {
    g.drawString(tekstDerSkalVises, 0, 10);
    g.drawString(tekstDerSkalVises, 5, 15);
    g.drawString(tekstDerSkalVises, 10,20);
  }
}

Ud fra kildeteksten ses, at bønnen har egenskaben tekst (der bestemmer, hvilken tekst der skal vises).

En javabønne siges at have en egenskab, hvis den har en tilsvarende get- og/eller set-metode

Bemærk at en javabønnes egenskaber ikke har nogen given relation til de objektvariabler, den måtte have. Således har bønnen egenskaben tekst, fordi den har metoden getTekst() og/eller setTekst(), og hvordan bønnen husker egenskaben internt (her sker det i den private variabel tekstDerSkalVises) er sagen uvedkommende, set udefra.

Visse udviklingsværktøjer - herunder JBuilder - kan kun håndtere bønner, der ligger i en pakke, så derfor er alle eksemplerne lagt i pakken 'vp' (husk, at de så også ligger i undermappen 'vp' i forhold til en kildetekstfil, der ikke lå i en pakke).

4.2.2 Brug af bønner fra et udviklingsværktøj

Så snart bønnen er oversat er den klar til brug og kan bruges fra udviklingsværktøjet.

I JBuilder kan det gøres ved at klikke på den lille kasse til venstre for komponenterne på design-fanen og vælge bønnen direkte.

Ellers skal du først definere et bibliotek, der indeholder bønnen:

  • I JBuilder: vælg 'Tools | Configure Libraries', vælg 'New..' og 'Add...', og angiv stien (roden) til .class-filerne.

  • I JDeveloper: vælg 'Project | Default project settings', under 'Libraries' vælg 'New...' og angiv stien (roden) til .class-filerne.

Nu kan du konfigurere paletten med 'Tools | Configure palette', vælg 'Add component', og vælg den fane, du ønsker komponenten skal vises på (f.eks. 'AWT').

Under 'Library' vælger du det bibliotek du lige har oprettet og derpå klassen i det.

Kode genereret af udviklingsværktøjet

Udviklingsværktøjet kan nu generere et program, der bruger bønnen.

Vælger man bønnens egenskaber, vil man se, at den har egenskaben tekst, ud over de, der er arvet fra Component (f.eks. forgrunds- og baggrundsfarve).

Den genererede kode ser således ud (for JBuilder og JDeveloper):

import vp.*;
import java.awt.*;

public class VindueMedGentagTekst extends Frame
{
  GentagTekst gentagtekst1 = new GentagTekst();

  public VindueMedGentagTekst() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void jbInit() throws Exception  {
    this.setSize(new Dimension(319, 247));
    this.setLayout(null);

    gentagtekst1.setTekst("ryst!");
    gentagtekst1.setBounds(new Rectangle(177, 190, 111, 28));
    this.add(gentagtekst1, null);
  }
}

4.2.3 Størrelsen af en grafisk komponent

Prøver man at bruge GentagTekst som en bønne, opdager man at den som udgangspunkt bliver meget lille (den får en bredde og højde på nul). Det hænger sammen med, at alle grafiske komponenter skal kunne fortælle deres omgivelser, hvor store de har behov for at være på skærmen for at kunne fungere korrekt.

Det sker ved, at omgivelserne kalder metoden getPreferredSize(), der skal returnere et Dimension-objekt med den foretrukne bredde og højde.

Herunder er endnu en grafisk komponent. Fordi vi har defineret getPreferredSize(), vil udviklingsværktøj og layout-managere så vidt muligt rette sig efter denne størrelse.

Layout-managere og deres virkemåde er beskrevet mere grundigt i afsnit 4.7.4.

package vp;
import java.awt.*;
public class BoenneMedForetrukkenStr extends Component
{
  Dimension foretrukneStørrelse = new Dimension(100,50);

  /** fortæl containeren hvad denne komponents foretrukne størrelse er */
  public Dimension getPreferredSize()
  {
    return foretrukneStørrelse;
  }

  public void paint(Graphics g)
  {
    Dimension str = getSize();  // faktisk størrelse (kan variere fra foretrukne)
    g.drawOval(0, 0, str.width, str.height);
  }
}

Ud over getPreferredSize() findes også getMinimumSize() og getMaximumSize():

  public Dimension getMinimumSize()
  public Dimension getMaximumSize()

Disse bruges (sammen med getPreferredSize()) af containerens layout-manager for at afgøre, hvordan containerens komponenter skal placeres indbyrdes. Det er altså, ligesom med pant()-metoden, ikke meningen, at man selv kalder dem, men at systemet (containerens layout-manager) kalder dem, når der er brug for det (f.eks. når vinduet ændrer størrelse).

4.2.4 Skelne mellem udviklings- og i kørselsfasen

Når man i udviklingsfasen bruger designværktøjet til at designe skærmbilleder og lægger en komponent (bønne) ind på skærmbilledet, sker der det, at værktøjet rent faktisk opretter bønnen (med new), sætter dens egenskaber tilsvarende (med kald af set-metoder) og viser bønnen på skærmen.

Nogle gange har en bønne imidlertid brug for at vide, om den lige nu er i gang med at blive brugt af et udviklingsværktøj under udviklingsfasen eller om den rent faktisk er en del af et kørende program. Det kunne f.eks. være relevant, hvis bønnen lagde beslag på nogle vigtige resurser eller hvis den havde et højt processor- eller hukommelsesforbrug.

En bønne kan undersøge, om den er i udviklingsfasen/designfasen (og altså kører i et udviklingsværktøj) eller i kørselsfasen ved at kalde metoden java.beans.Beans.isDesignTime():

package vp;
import java.awt.*;
public class BoenneGenkenderDesignfase extends Component
{
  Dimension foretrukneStørrelse = new Dimension(100,50);

  public Dimension getPreferredSize()
  {
    return foretrukneStørrelse;
  }

  public void paint(Graphics g)
  {
    if (java.beans.Beans.isDesignTime())
    {
      g.drawString("Designfase",5,15);
    } else {
      g.drawString("Under kørsel",5,15);
    }
  }
}

4.3 Karakteristika ved javabønner

Der er visse karakteristika ved javabønner, som gør det muligt at behandle dem som afgrænsede komponenter og konfigurere dem med et udviklingsværktøj. Disse er ridset op i det følgende.

4.3.1 Bønner skal have en parameterløs konstruktør

Bønner skal have en konstruktør uden parametre, dvs. de skal kunne oprettes som f.eks.:

  GentagTekst gentagtekst1 = new GentagTekst();

Dette er, for at udviklingsværktøjet kan oprette komponenterne på en ensartet måde.

Prøv, for at illustrere nødvendigheden af dette krav, at forestille dig, at kravet ikke fandtes: Så skulle udviklingsværktøjet kunne håndtere klasser med meget indviklede konstruktører, f.eks. en med 17 parametre, hvor parameter nummer 6 i øvrigt altid skal være større end parameter 7 og parameter 16 være et lige tal større end 38. Et menneske kunne selvfølgelig læse dokumentationen og forstå kravene, mens det er helt umuligt for værktøjet at imødekomme sådanne krav.

I stedet for at angive startværdierne i konstruktøren sættes de som egenskaber.

4.3.2 Bønner kan have egenskaber

Egenskaberne læses/sættes med de kendte get- og set-metoder.

Disse kan være simple som f.eks.:

  public void setTekst(String t)
  public String getTekst()

Egenskaberne bør være uafhængige, forstået på den måde at bønnen ikke kan antage at en bestemt egenskab bliver sat eller at egenskaberne bliver sat i en bestemt rækkefølge.

Arver man fra en bønne, vil de arvede egenskaber indgå på lige fod med de nye egenskaber.

Bønnens egenskaber kan være af en vilkårlig type, men værktøjer understøtter som regel kun egenskaber af de simple typer (int, double, ...) og objekter af type String, Color, Point, Dimension og et par stykker til. Ønsker man, at brugeren kan redigere egenskaber af andre typer må man selv programmere, hvordan det skal ske (se næste afsnit).

Indekserede egenskaber

Egenskaber kan også være indekserede (dvs. i et array) som f.eks.:

  public void setTekst(String t[])
  public String[] getTekst()

Ofte vil der da også være mulighed for at arbejde med en enkelt indgang, f.eks.:

  public void setTekst(int indeks, String t)
  public String getTekst(int indeks)

Bundne og begrænsede egenskaber

Nogen gange er det hensigtsmæssigt, at andre kan få at vide, når en egenskab bliver ændret. Til det formål kan egenskaber være bundne (eng.: bound) eller begrænsede (eng.: constrained).

Når bundne egenskaber ændres, afstedkommer det en hændelse (af typen PropertyChangeEvent). Disse hændelser kan man lytte efter (ved at kalde addPropertyChangeListener() på komponenten med en lytter af typen PropertyChangeListener som parameter). Man kan altså få at vide, når en bestemt egenskab ændres, ved at "abonnere" på den, og det foregår på præcis samme måde som med andre slags hændelser.

Begrænsede egenskaber er som bundne egenskaber, men hændelserne, der sendes, er i stedet af typen VetoableChangeEvent. Lytterne (der er af typen VetoableChangeListener) har lov til at kaste undtagelsen PropertyVetoException for at annullere ændringen af egenskaben.

4.3.3 Bønner kan have tilknyttet ekstra information

Ekstra information om en bønne ligger i en såkaldt BeanInfo-klasse2. F.eks. kunne GentagTekst.java have tilknyttet klassen GentagTekstBeanInfo.java til ekstra information.

Denne information er kun til hjælp for udviklingsværktøjet. Hvis der ikke er en BeanInfo-klasse, bruges introspektion (beskrevet i kapitel 11), dvs. inspicering af bønne-klassens metoder (egenskaber). Introspektion er oftest tilstrækkeligt, så BeanInfo-klasser er ikke så almindelige. De ekstra informationer, der kan specificeres i en BeanInfo-klasse, er:

  • Hvilket ikon bønnen skal repræsenteres af i værktøjet

  • Hvilke egenskaber der findes, og for hver egenskab en beskrivelse og hvordan den aflæses og sættes

  • Hvordan de redigeres. Der kan tilknyttes en klasse, der bestemmer f.eks.:

  • Hvilke værdier der er mulige

  • Hvilken javakode der skal sættes ind i kildeteksten

  • Om et skræddersyet redigeringsvindue skal dukke op

4.3.4 Bønner bør være afgrænsede og uafhængige

Det siger sig selv, at en komponent, der skal kunne bruges igen og igen i mange sammenhænge, skal være fuldstændig afgrænset og uafhængig af omgivelserne. Er den afhængig af dele af resten af programmet, kan den jo netop ikke bruges uden disse programdele.

4.3.5 Bønner kan understøtte hændelses-lyttere

Mange bønner har brug for at fortælle resten af programmet (selvom bønnen selvfølgelig ikke aner, hvilket program det måtte være), at der er sket noget.

Det går ikke, at bønnen kalder en metode i det omkringliggende program, for så ville bønnen ikke virke i andre sammenhænge.

I stedet skal det omgivende program registrere et lytter-objekt hos bønnen, som bønnen skal huske. Når den får brug for at fortælle resten af programmet, at "nu er der er sket noget" kalder den lytteren.

Det bedst kendte eksempel på dette er måske Button. For at få at vide, når der trykkes på den, skal vi registrere et ActionListener-objekt hos den. Når der trykkes på knappen, vil den kalde metoden actionPerformed() på lytteren med et ActionEvent-objekt som parameter, der beskriver hændelsen.

Hvis du nu skal til at programmere din første komponent nu, så bemærk, at du har skiftet rolle, jvf. afsnit 4.2: Før har du kun anvendt andre komponenter og i den forbindelse derfor måske kaldt f.eks. addActionListener().

Nu laver du selv komponenter og skal derfor definere f.eks. addActionListener(), hvis du vil have, at dine komponenter understøtter denne slags hændelser. Tilsvarende bør du definere en removeActionListener(), der fjerner en lytter fra din komponent, så at sige "annullerer abonnementet" på hændelserne.

Det vil også være en god idé internt at huske, hvilke lyttere der er registreret, og definere en intern metode, der kalder actionPerformed() på alle lytterne, når en hændelse skal affyres (man kunne passende kalde metoden for sendActionPerformedTilLytterne()).

Herunder et eksempel på en komponent, der sender Action-hændelser. De sendes, hver gang dets paint()-metode kaldes (dette er ikke specielt hensigtsmæssigt, men giver et simpelt eksempel3).

import java.awt.event.*;
import java.util.*;
import java.awt.*;

/** En komponent, der sender en hændelse hver gang den gentegnes.
 *  Du kan vælge at kopiere kildeteksten ind i dit eget program.
 *  Hændelsen, der sendes er ActionEvent. Denne hændelse indeholder bl.a.:
 *  <ul>
 *  <li> et objekt (kilden til hændelsen)
 *  <li> en streng (beskrivelse)
 *  <li> et tal (et ID)
 *  </ul>
 *  Du kan selvfølgelig selv bestemme hvad objektet, strengen og tallet er,
 *  hvis det er dine egne klasser der lytter efter hændelserne.
 */
public class SenderActionEvent extends Component
{
  // ---------------------------- kopier herfra ----------------------------
   /** Lyttere til denne bønne */
   private ArrayList lyttere = new ArrayList(2);

   /** Tilføjer en lytter. Lytteren vil få kaldt metoden actionPerformed() når
   *  der sker en hændelse.
   *  @param l Lytteren, der skal tilføjes denne komponent.
   */
   public synchronized void addActionListener(ActionListener l)
   {
     lyttere.add(l);
  }

   /** Fjerner en lytter */
   public synchronized void removeActionListener(ActionListener l)
   {
     lyttere.remove(l);
  }

   /** Sender en hændelse til lyttere. Lytterne er de, der tidligere er blevet
   *  tilføjet med kald til addActionListener()
   *  @see #addActionListener(ActionListener)
   *  @param hændelse Hændelsen med de data, der skal sendes til lytterne
   */
   protected void sendActionPerformedTilLytterne(ActionEvent hændelse)
   {
     for (Iterator i=lyttere.iterator(); i.hasNext(); )
     {
       ActionListener l = (ActionListener) i.next();
       l.actionPerformed(hændelse);
    }
  }
  // ---------------------------- kopier hertil ----------------------------

  public void paint(Graphics g)
  {
    // opret en Action-hændelse, der kommer fra denne komponent og send den
    // brug konstruktøren new ActionEvent( afstanderobjekt, id, beskrivelse)
    ActionEvent hændelse = new ActionEvent(this, 0, "paint() kaldt");
    sendActionPerformedTilLytterne(hændelse);
  }
}

Koden fra eksemplet kan kopieres og genbruges i dine egne komponenter.

Action-hændelser er den mest anvendte hændelsestype, men hvis du vil understøtte en anden type, skal du blot ændre alle de steder, hvor der står 'Action' (til f.eks. 'Mouse').

4.4 Ekstra eksempler

Her kommer nogle flere eksempler på komponenter. Alle kan lægges ind i et udviklingsværktøjs palette og manipuleres fuldstændigt som de forud installerede komponenter.

4.4.1 Rystetekst

Den følgende bønne viser en tekst, der ryster. Det gøres ved, at den starter en separat tråd, som 10 gange i sekundet kalder repaint() for at få komponenten gentegnet. I paint() tegnes en tekst med en tilfældig forskydning, og når komponenten gentegnes 10 gange i sekundet, ser det ud, som om teksten ryster.

Samtidig kan denne bønne fortælle containeren, hvad dens foretrukne størrelse er (metoden getPreferredSize() beskrevet i afsnit 4.2.3).

Det sker ved at undersøge den pågældende tekst og finde ud af, hvor bred og høj den er med den pågældende skrifttype-størrelse (font-metrik).

package vp;
import java.awt.*;
public class Rystetekst extends Component implements Runnable
{
  private String tekst = "rystetekst";
  public void setTekst(String t) { tekst = t; foretrukneStørrelse = null; }
  public String getTekst() { return tekst; }

  Dimension foretrukneStørrelse = null;

  public Dimension getPreferredSize() {
    if (foretrukneStørrelse == null) try {
      FontMetrics fm = getFontMetrics(getFont());
      int tbr = fm.stringWidth(tekst);                         // tekstbredde
      int thø = fm.getHeight();                                // teksthøjde
      foretrukneStørrelse = new Dimension(tbr + 10, thø + 10); // lidt ekstra
    } catch (Exception e) {
      e.printStackTrace();
      foretrukneStørrelse = new Dimension(150,50);
    }
    return foretrukneStørrelse;
  }

  public Rystetekst()
  {
    // hvis ikke i designfase så start en tråd der tager sig af opdateringen
    if (!java.beans.Beans.isDesignTime())
    {
      Thread tråd = new Thread(this);
      tråd.setDaemon(true);    // systemet skal ikke vente på at tråden stopper
      tråd.start();            // ny vil ny tråd starte nede i run()-metoden
    }
  }

  /** sørger for at kalde repaint() regelmæssigt */
  public void run() {
    try {
      while (true) 
      {
        Thread.sleep(100);     // vent 1/10 sekund
        repaint();             // gentegn komponenten
      }
    } catch (Exception e) {}
  }

  public void paint(Graphics g)
  {
    // tegn tekst på tilfældig x- og y-koordinat
    g.drawString(tekst,            (int)(Math.random()*10), 
                       getHeight()-(int)(Math.random()*10));
  }
}

4.4.2 Rulletekst

Her er endnu en grafisk bønne, der lader en tekst rulle vandret hen over skærmen. Den har derfor egenskaberne tekst og fart. Derudover er der opdateringstid, der bestemmer, hvor hyppigt den helst skal gentegnes på skærmen.

package vp;
import java.awt.*;
import java.util.*;

public class Rulletekst extends Component implements Runnable
{
  /** fortæl containeren hvad denne komponents foretrukne størrelse er */
  public Dimension getPreferredSize() { return new Dimension(100,15); }

  // Egenskaber
  private String tekst = "rulletekst ";
  private int fart = 10;           // antal punkter der rykkes i sekundet
  private int opdateringstid = 50; // antal millisekunder mellem hver gentegning

  public void setTekst(String t) { tekst = t; klar = false; }
  public String getTekst() { return tekst; }

  public void setFart(int f) { fart = f; }
  public int getFart() { return fart; }

  public void setOpdateringstid(int f) {
    if (f>=10) opdateringstid = f; // tillad ikke under 10 msek
    else opdateringstid = 10;
  }

  public int getOpdateringstid() { return opdateringstid; }

  // Interne variabler
  private boolean klar = false;
  private int tbr, thø;            // tekstens bredde og højde i punkter
  private String tekstx;           // teksten i det nødvendige antal kopier

  private synchronized void gørKlar()
  {
    FontMetrics fm = getFontMetrics(getFont());
    tbr = fm.stringWidth(tekst);
    thø = fm.getHeight();
    Dimension d = getSize(); // komponentens størrelse
    int antalKopier = 2*d.width/tbr + 1;
    tekstx = tekst;
    while (antalKopier-- >= 1) tekstx = tekstx + tekst;

    Thread tråd = new Thread(this);
    tråd.setDaemon(true);
    tråd.start();
    klar = true;
  }

  /** sørger for at kalde repaint() regelmæssigt */
  public void run() {
    try {
      while (true) {
        Thread.sleep(opdateringstid);
        repaint();
      }
    } catch (Exception e) {}
  }

  public void paint(Graphics g)
  {
    if (!klar) gørKlar();

    int x = (int) (System.currentTimeMillis()*fart/1000);
    g.drawString(tekstx, x%tbr-tbr, thø);
  }
}

4.4.3 Simpel kryptering

Javabønner behøver ikke at være grafiske. Her er en ikke-grafisk bønne, der kan kode tekster med en gammel simpel metode (Cæsar-kodning): Man erstatter a med b, b med c, c med d, ... æ med å og å med a.

Ønsker man en anden rækkefølge af bogstaverne, ændrer man egenskaben kodestreng.

Egenskaben hop bruges til at specificere kodningens retning (1 svarer til indkodning, -1 til afkodning).

Metoden kod() udfører den faktiske kodning. Den svarer ikke til en egenskab (med get- og set-metoder), og programmøren, der anvender bønnen, må derfor selv sørge for at lave koden, der kalder metoden, når der er brug for det.

package vp;
public class Koder
{
  private String kodestreng = "abcdefghijklmnopqrstuvwxyzæøå";
  private int hop = 1;
  
  public String getKodestreng() { return kodestreng; }
  public void setKodestreng(String ny) { kodestreng = ny; }
  
  public void setHop(int ny) { hop = ny; }
  public int getHop() {  return hop; }

  public String kod(String s)
  {
    StringBuffer sb = new StringBuffer();
    for (int i=0; i<s.length(); i++)
    {
      int p = kodestreng.indexOf( s.charAt(i) );
      if (p>=0) {
        p = (p + hop + kodestreng.length()) % kodestreng.length();
        sb.append( kodestreng.charAt( p ));
      } else sb.append( s.charAt(i) );
    }
    return sb.toString();
  }
}

4.4.4 Eksempel på brug af bønnerne i et værktøj

På figuren herunder er vi i gang med at designe et lille program med bønnerne Rystetekst, Rulletekst og Koder (der er lagt ind i 'Other'-fanen i udviklingsværktøjet JBuilders palette). Vi har lige nu markeret bønnen Rystetekst, og værktøjet viser derfor egenskaben tekst til højre.

Da bønnen Koder ikke er grafisk, vises den ikke i værktøjets design-fane. I stedet må man vælge den i struktur-træet (nederst til venstre er instanserne indkoder og afkoder af Koder-bønnen vist).

Programmet viser nogle ryste- og rulletekster og to indtastningsfelter. Taster man noget i feltet til venstre og trykker retur, vil teksten blive kodet med en Koder-bønne og resultatet vist i feltet til højre.

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

public class BenytRystetekstRulletekstKoder extends Frame
{
  Rystetekst rystetekst1 = new Rystetekst();
  Rystetekst rystetekst2 = new Rystetekst();

  Rulletekst rulletekst1 = new Rulletekst();
  Rulletekst rulletekst2 = new Rulletekst();
  Rulletekst rulletekst3 = new Rulletekst();

  Koder indkoder = new Koder();
  Koder afkoder = new Koder();

  JTextField jTextField1 = new JTextField();
  JTextField jTextField2 = new JTextField();


  public BenytRystetekstRulletekstKoder() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void jbInit() throws Exception {
    rulletekst1.setTekst("Hej, jeg hedder Jacob Nordfalk.   ");
    rulletekst1.setForeground(Color.red);
    rulletekst1.setFart(-50);
    rulletekst1.setOpdateringstid(20);
    rulletekst2.setOpdateringstid(100);
    rulletekst3.setTekst("Indtast teksten i feltet til venstre herunder og " +
    "tryk retur. Så kommer den indkodede tekst til højre. Du kan også taste " +
    "noget til højre og få det afkodet til venstre.");
    rulletekst3.setFart(-20);

    afkoder.setHop(-1);

    jTextField1.setText("Tast her");
    jTextField1.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(ActionEvent e) {
        jTextField1_actionPerformed(e);
      }
    });
    jTextField2.setText("Her kommer koden");
    jTextField2.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(ActionEvent e) {
        jTextField2_actionPerformed(e);
      }
    });
    
    this.setLayout(null);
    this.setSize(new Dimension(319, 247));

    this.add(rystetekst1, null);                         // tilføj til container
    this.add(rystetekst2, null);
    this.add(rulletekst1, null);
    this.add(rulletekst2, null);
    this.add(rulletekst3, null);
    this.add(jTextField1, null);
    this.add(jTextField2, null);

    rystetekst1.setBounds(new Rectangle(15, 6, 111, 28)); // placering på skærm
    rystetekst2.setBounds(new Rectangle(183, 3, 112, 35));
    rulletekst1.setBounds(new Rectangle(81, 38, 152, 22));
    rulletekst2.setBounds(new Rectangle(0, 224, 320, 19));
    rulletekst3.setBounds(new Rectangle(12, 110, 290, 22));
    jTextField1.setBounds(new Rectangle(10, 137, 138, 25));
    jTextField2.setBounds(new Rectangle(160, 137, 137, 25));
  }

  void jTextField1_actionPerformed(ActionEvent e) {
    String s = e.getActionCommand();
    s = indkoder.kod(s);
    jTextField2.setText(s);
  }

  void jTextField2_actionPerformed(ActionEvent e) {
    String s = e.getActionCommand();
    s = afkoder.kod(s);
    jTextField1.setText(s);
  }

  public static void main(String[] arg) {
    BenytRystetekstRulletekstKoder v = new BenytRystetekstRulletekstKoder();
    v.setSize(400,400);
    v.setVisible(true);
  }
}

4.5 Opgaver

I alle opgaverne herunder er det underforstået, at du sørger for at afprøve dine ting ved at lave små programmer, der bruger komponenterne.

Vejledende løsninger på nogen af opgaverne findes i næste afsnit.

Grafiske komponenter

  1. Ret i bønnen GentagTekst, og indfør egenskaben antal, der bestemmer, hvor mange gange teksten skal tegnes.

  2. Ret bønnen, så den fortæller sin foretrukne størrelse til containeren (se afsnit 4.2.3, Størrelsen af en grafisk komponent).

  3. Kig på bønnen Rystetekst i afsnit 4.4.1. Hvad hvis man skulle kunne styre bønnens opdateringstid, der bestemmer, hvor hurtigt teksten ryster (tidsrummet mellem to gentegninger)? Indfør egenskaben opdateringstid i bønnen, og brug den.

  4. Blandt AWT-komponenterne mangler der en komponent, der kan vise et billede4.
    Lav en, f.eks. med udgangspunkt i GentagTekst.java. Definér egenskaben
    filnavn en streng, der beskriver, hvor billedet er
    Billeder kan hentes med f.eks.:
    Image i = Toolkit.getDefaultToolkit().getImage("hej.jpg");
    g.drawImage(i, 10, 10, this);

    Her skal filen hej.jpg ligge samme sted, som programmet udføres (ellers prøv at kopiere billedet til nogle forskellige steder i filstrukturen, indtil programmet 'får fat' i det).

  5. Lav en grafisk komponent, der viser en animation, dvs. et antal billeder vist i kort rækkefølge efter hinanden. Kig på Rulletekst.java for nogle idéer.
    Har du ikke lavet den forrige opgave, så tag udgangspunkt i Swing-komponenten JLabel (der ud over en tekst også kan vise et billede).

Webserver-komponent

  1. Lav en (ikke-grafisk) webserver-bønne. Tag udgangspunkt klasserne FlertraadetHjemmesidevaert og Anmodning fra http://javabog.dk/OOP/eksempler/kapitel_17/ (beskrevet i http://javabog.dk/OOP/kapitel16.html og det efterfølgende kapitel).
    Den skal have egenskaberne:
    port angiver, hvilken port serveren skal lytte på (f.eks. 8080)
    aktiv om den er aktiv, dvs. om den venter på anmodninger (true/false)
    Lad den først bare svare med den samme tekst uafhængig af spørgsmålet.
    Opret et testprogram, der anvender bønnen og sætter dens egenskaber.

  2. Lad den understøtte abonnement på Action-hændelser (se afsnit 4.3.5), på en sådan måde, at der sendes en hændelse, hver gang der kommer en anmodning.
    Ændr i testprogrammet så det abonnerer på hændelsen og skriver ud, når den indtræffer.

  3. Lad bønnen kunne læse filer fra filsystemet og sende til brugeren. Definér egenskaben
    rod sti til rodkataloget, hvor HTML-siderne, der kan hentes, er (f.eks. C:\HTML).

4.6 Løsninger

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.6.1 Grafisk komponent: GentagTekst

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.6.2 Grafisk komponent: Billede

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.6.3 Webserver-komponent

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7 Avanceret

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.1 En komponent til at tegne kurver

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.2 Repræsentation af funktioner

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.3 Fortolkning af strenge til funktioner

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.4 Layout-manageres virkemåde

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.5 Øvelse: Samspil med layout-manager

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.6 Opgave: Adresseindtastningskomponent

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

4.7.7 Løsning: Adresseindtastningskomponent

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

1En anonym klasse er en klasse uden navn, som der oprettes netop ét objekt ud fra der, hvor den defineres (altså en unavngiven lokal klasse). Mere information om anonyme klasser kan f.eks. findes i kapitlet 'Avancerede klasser' i 'Objektorienteret programmering i Java' af Jacob Nordfalk, som også kan læses på http://javabog.dk

2Dette skulle svare til et type-library i Windows' COM-verden.

3Normalt sendes der ikke hændelser, fordi paint() kaldes. Se klassen Kontomodel i afsnit 19.3.1 for et mere realistisk eksempel.

4Dette gælder ikke Swing, hvor man kan sætte billeder på næsten alle komponenterne. For eksempel kan JLabel ud over en tekst også vise et billede.

5Dette er et eksempel på Rekursiv Komposition (se afsnit 18.5).

javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (71% af værket).

Ønsker du at se de sidste 29% af dette værk (362838 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.