© 1999-2003, Flemming Koch Jensen
Alle rettigheder forbeholdt
Menuer

Opgaver

 

 

Et vindue har ofte en menubar med menuer, der giver mulighed for at vælge forskellige handlinger man ønsker udført, i forbindelse med den applikation som vinduet tilhører. Vi vil derfor studere mulighederne for at lave og anvende sådanne menuer i Java.
Det er en større proces at implemnetere en menubar, og vi vil derfor arbejde os frem mod målet i flere mindre skridt.
Følgende vindue:
Figur 1:
Vindue med tom menubar
Laves af programmet:
Source 1:
Tilføjelse af JMenuBar
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

class MenuFrame extends JFrame {
  
  public MenuFrame( String titel ) {
    super( titel );
    
    setJMenuBar( makeMenuBar() );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    setSize( 300, 100 );
    setVisible( true );
  }
  
  /*
   *   Lave menubar med menuer
   */
  private JMenuBar makeMenuBar() {
    JMenuBar bar = new JMenuBar();
    
    //...
    
    return bar;
  }

}
Vi vælger selv at sætte størrelsen af vinduet, da vi i første omgang ikke er interesseret i at tilføje det komponenter.
Hvis man ser godt efter i figur 1, kan man svagt ane, at der er et smalt lysegråt område (nærmest en linie) under vinduets hoved. Denne linie er en tom menubar. Vi har endnu ikke føjet menuer til menubaren og den er derfor "klappet sammen".
Man ser at vi har valgt at placere opbygningen af menubaren i en intern service-metode. Det gør man normalt, da opbygningen af en menu kan fylde endog mange linier, og det derfor er en fordel at få disse linier ud af selve kontruktoren. Den eneste linie i konstruktoren som vedrører menubaren, vil derfor være linie med kaldet af setJMenuBar, der føjer menubaren til framen. I det følgende vil vi derfor udelukkende beskæftige os med indholdet af metoden makeMenuBar.
En menubar er i Java en instans af JMenuBar. JMenuBar er en container, som kan indeholde menuer. Disse menuer placeres hen ad menubaren i den rækkefølge de er blevet tilføjet, med den sædvanlie container-metode add.
Hver enkelt menu er en instans af JMenu, der ligeledes er en container. En JMenu har en konstruktor, der tager en String som parameter. Denne parameter er navnet på menuen.
Hvis vi udbyggede makeMenuBar med:
Source 2:
JMenuBar
med to menuer
private JMenuBar makeMenuBar() {
  JMenuBar bar = new JMenuBar();
  
  JMenu fileMenu = new JMenu( "File" );
  JMenu helpMenu = new JMenu( "Help" );
  
  bar.add( fileMenu );
  bar.add( helpMenu );
  
  return bar;
}
Vores vindue får nu følgende udseende med de to menuer i menubaren:
Figur 2:
Vindue med to menuer
Nederst har vi aktiveret den første menu, hvorved den bliver blå, men da den endnu ikke indeholder nogen menu-punkter er der kun en lille "tap", hvor indholdet skulle have været.
Som nævnt er en JMenu en container. En JMenu kan indeholde instanser af JMenuItem eller JMenu (se evt. Composite Pattern for den generelle design idé). I det sidste tilfælde vil der være tale om undermenuer.
Lad os føje nogle JMenuItem's til den første af menuerne:
Source 3:
JMenuItem
's i menu
private JMenuBar makeMenuBar() {
  JMenuBar bar = new JMenuBar();
  
  JMenu fileMenu = new JMenu( "File" );

    JMenuItem newMI = new JMenuItem( "New" );
    fileMenu.add( newMI );
  
    JMenuItem openMI = new JMenuItem( "Open" );
    fileMenu.add( openMI );
  
    JMenuItem closeMI = new JMenuItem( "Close" );
    fileMenu.add( closeMI );
  
    fileMenu.addSeparator();
  
    JMenuItem saveMI = new JMenuItem( "Save" );
    fileMenu.add( saveMI );
  
  JMenu helpMenu = new JMenu( "Help" );
  
  bar.add( fileMenu );
  bar.add( helpMenu );
  
  return bar;
}
Det er nyttigt at anvende indrykninger for at fremhæve sammenhængen mellem menuer og deres indhold.
Menuen før følgende udseende:
Figur 3:
Menu med menu-punkter
Den kosmetiske streng der adskiller de tre øverste menu-punkter fra det nederste, laves ved at kalde metoden addSeparator på det ønskede sted i sekvensen af add-kald.

 

1. Event-håndtering

Når brugeren vælger et menu-punkt udløses der en ActionEvent. Når vi ønsker at modtage events vedrørende valg af menu-punkter, skal vi blot tilmelde os som ActionListener hos alle menu-punkter og gemme en reference til dem, for senere identifikation af kilden til event'en.
For at illustrere teknikken kun vi lade programmet skrive hvilket menu-punkt, der er tale om, når det bliver valgt:
Source 4:
Events fra menu-punkter
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

class MenuFrame extends JFrame implements ActionListener {
  
  private JMenuItem newMI, openMI, closeMI, saveMI;
  
  public MenuFrame( String titel ) {
    super( titel );
    
    setJMenuBar( makeMenuBar() );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    setSize( 300, 100 );
    setVisible( true );
  }
  
  /*
   *   Lave menubar med menuer
   */
  private JMenuBar makeMenuBar() {
    JMenuBar bar = new JMenuBar();
    
    JMenu fileMenu = new JMenu( "File" );
  
      newMI = new JMenuItem( "New" );
      newMI.addActionListener( this );
      fileMenu.add( newMI );
    
      openMI = new JMenuItem( "Open" );
      openMI.addActionListener( this );
      fileMenu.add( openMI );
    
      closeMI = new JMenuItem( "Close" );
      closeMI.addActionListener( this );
      fileMenu.add( closeMI );
    
      fileMenu.addSeparator();
    
      saveMI = new JMenuItem( "Save" );
      saveMI.addActionListener( this );
      fileMenu.add( saveMI );
    
    JMenu helpMenu = new JMenu( "Help" );
    
    bar.add( fileMenu );
    bar.add( helpMenu );
    
    return bar;
  }
  
  public void actionPerformed( ActionEvent e ) {
    Object source = e.getSource();
    
    if ( source == newMI )
      System.out.println( "new" );
    else if ( source == openMI )
      System.out.println( "open" );
    else if ( source == closeMI )
      System.out.println( "close" );
    else if ( source == saveMI )
      System.out.println( "save" );
  }

}
Prøv:
Kør eksemplet - vælg nogle af menu-punkterne!

Instanser af JMenuItem er ikke de eneste menu-punkter man kan føje til en menu. Der findes to andre klasser: JCheckBoxMenuItem og JRadioButtonMenuItem, der subklasser af JMenuItem, som også kan tilføjes en JMenu JMenu er selv en subklasse af JMenuItem, i det der fremstår som et Composite Pattern:

Figur 4:
JMenuItem og dens subklasser

 

2. Undermenuer

Det er enkelt at lave undermenuer. En undermenu er ganske enkelt en JMenu, der er add'ed til en JMenu som ethvert andet JMenuItem.
F.eks.:
Figur 5:
Undermenu
Der laves med følgende implementation af makeMenuBar:
Source 5:
Menu med en undermenu
private JMenuBar makeMenuBar() {
  JMenuBar bar = new JMenuBar();
  
  JMenu hovedMenu = new JMenu( "Hoved menu titel" );
  
    hovedMenu.add( new JMenuItem( "Hoved menu item 1" ) );

      JMenu underMenu = new JMenu( "Under menu titel" );
    
      underMenu.add( new JMenuItem( "Under menu item 1" ) );
      underMenu.add( new JMenuItem( "Under menu item 2" ) );
      underMenu.add( new JMenuItem( "Under menu item 3" ) );

    hovedMenu.add( underMenu );

    hovedMenu.add( new JMenuItem( "Hoved menu item 2" ) );
  
  bar.add( hovedMenu );
  
  return bar;
}

 

3. JCheckBoxMenuItem

Menu-punkter, som er instanser af JCheckBoxMenuItem, har et tag som enten kan være sat eller ej. Det betyder at menu-punktet fungerer som en kontakt, der enten kan være slået til eller fra.
Betragt følgende menu:
Figur 6:
Menu med JCheckBox-MenuItem
s
Her optræder der en firkant til venstre for menu-punktet. Denne firkant giver mulighed for at et menu-punkt kan være valgt eller ej. Hvis vi vælger de to menu-punkter: Skrå skrift og Fed skrift, bliver billedet:
Figur 7:
To valgte JCheckBox-MenuItem
s
Default er JCheckBoxMenuItems ikke valgt, men ønsker man at et sådant menu-punkt fra starten skal være valgt kan man bruge metoden setState, der har en tilsvarende get-metode:
boolean getState()
void setState( boolean b )
Lad os for eksemplets skyld se en implementation af makeMenuBar, der laver ovenstående vindue og initielt sætter de to af menu-punkterne, svarende til ovenstående figur:
Source 6:
Menu med tre JCheckBox-MenuItems
private JMenuBar makeMenuBar() {
  JMenuBar bar = new JMenuBar();
  
  JMenu typoMenu = new JMenu( "Typografi" );

    skråMI = new JCheckBoxMenuItem( "Skrå skrift" );
    skråMI.setState( true );
    typoMenu.add( skråMI );
  
    underMI = new JCheckBoxMenuItem( "Underlinieret" );
    underMI.setState( false );
    typoMenu.add( underMI );
  
    fedMI = new JCheckBoxMenuItem( "Fed skrift" );
    fedMI.setState( true );
    typoMenu.add( fedMI );
  
  bar.add( typoMenu );
  
  return bar;
}
Item-Listener I forbindelse med event-håndtering kan man vælge enten at tilmelde sig som ItemListener eller som ActionListener. Det er ligegyldigt hvilken man vælger. Det skyldes at en ændring af tilstanden (valgt/ikke valgt) altid hænger sammen med om menu-punktet aktiveres. Hvis der andre dele af systemet der kan ændre menu-punktets tilstand skal man tilmelde sig som ItemListener, ellers er det uden betydning.
Vi vil for eksemplets skyld vælge at være ItemListener, da vi allerede har optrådt som ActionListener i dette kapitel.
Som ItemListener skal vi implementere metoden:
void itemStateChanged( ItemEvent e )
Hvis vi indskrænker os til kun at meddele ændringens karakter, får det samlede program følgende udseende:
Source 7:
Håndtering af events fra de tre menu-punkter
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

class MenuFrame extends JFrame implements ItemListener {
  
  private JCheckBoxMenuItem skråMI, underMI, fedMI;
  
  public MenuFrame( String titel ) {
    super( titel );
    
    setJMenuBar( makeMenuBar() );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    setSize( 300, 100 );
    setVisible( true );
  }
  
  /*
   *   Lave menubar med menuer
   */
  private JMenuBar makeMenuBar() {
    JMenuBar bar = new JMenuBar();
    
    JMenu typoMenu = new JMenu( "Typografi" );
  
      skråMI = new JCheckBoxMenuItem( "Skrå skrift" );
      skråMI.setState( true );
      skråMI.addItemListener( this );
      typoMenu.add( skråMI );
    
      underMI = new JCheckBoxMenuItem( "Underlinieret" );
      underMI.setState( false );
      underMI.addItemListener( this );
      typoMenu.add( underMI );
    
      fedMI = new JCheckBoxMenuItem( "Fed skrift" );
      fedMI.setState( true );
      fedMI.addItemListener( this );
      typoMenu.add( fedMI );
    
    bar.add( typoMenu );
    
    return bar;
  }
  
  public void itemStateChanged( ItemEvent e ) {
    Object source = e.getSource();
    
    if ( source == skråMI )
      System.out.println( "skrå: " + skråMI.getState() );
    else if ( source == underMI )
      System.out.println( "under: " + underMI.getState() );
    else if ( source == fedMI )
      System.out.println( "fed: " + fedMI.getState() );
  }
}
Prøv:
Kør eksemplet - slå nogle af menu-punkterne fra og til!

 

4. JRadioButtonMenuItem

Instanser af JRadioButtonMenuItem ligner meget instanser af JRadioButton. Vi vil derfor ikke beskrive egenskaberne ved JRadioButtons, men kun beskrive implementationen, idet der henvises til kapitlet om JRadioButton.
JRadioButtonMenuItem har de samme konstruktorer som JRadioButton og de har samme funktionalitet.
Som for JCheckBoxMenuItem kan vi vælge mellem at være ActionListener eller ItemListener. Vi vælger igen at være ItemListener, da vi ellers vil få event hvis brugeren vælger et menu-punkt der allerede er valgt. Det er tilstandsændringer vi er interesseret i, og som ItemListener er det kun dem vi hører om.
Lad os se et eksempel. Betragt følgende vindue:
Figur 8:
Tre
radiobuttons i vindue
Dette vindue laves med følgende frame
Source 8:
Frame med tre JRadioButton-MenuItems
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

class MenuFrame extends JFrame implements ItemListener {
  
  private JRadioButtonMenuItem p8MI, p10MI, p12MI;
  
  public MenuFrame( String titel ) {
    super( titel );
    
    setJMenuBar( makeMenuBar() );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    setSize( 300, 100 );
    setVisible( true );
  }
  
  /*
   *   Lave menubar med menuer
   */
  private JMenuBar makeMenuBar() {
    JMenuBar bar = new JMenuBar();
    
    JMenu typoMenu = new JMenu( "Font størrelse" );
  
      ButtonGroup gruppen = new ButtonGroup();
  
      p8MI = new JRadioButtonMenuItem( "8", true );
      gruppen.add( p8MI );
      p8MI.addItemListener( this );
      typoMenu.add( p8MI );
    
      p10MI = new JRadioButtonMenuItem( "10" );
      gruppen.add( p10MI );
      p10MI.addItemListener( this );
      typoMenu.add( p10MI );
    
      p12MI = new JRadioButtonMenuItem( "12" );
      gruppen.add( p12MI );
      p12MI.addItemListener( this );
      typoMenu.add( p12MI );
    
    bar.add( typoMenu );
    
    return bar;
  }
  
  public void itemStateChanged( ItemEvent e ) {
    Object source = e.getSource();
    
    if ( source == p8MI )
      System.out.println( "8: " + p8MI.isSelected() );
    else if ( source == p10MI )
      System.out.println( "10: " + p10MI.isSelected() );
    else if ( source == p12MI )
      System.out.println( "12: " + p12MI.isSelected() );
  }
}
Som det ses bruger vi en ButtonGroup til at opnå indbyrdes udelukkelse af de tre radiobuttons; hvilket er det almidelige for JRadioButtons.

 

5. Shortcuts

Det er muligt at lave to former for shortcuts til menu-punkter: mnemonics og accelerators. Det skal bemærkes at der alle steder i det følgende anvendes store bogstaver for taster. Shortkeys er ikke case-sensitive, men det er gjort for at det skulle blive ensartet.

 

5.1 Mnemonics

Alt-tasten Alle mnemonics består af Alt-tasten og et bogstav. Det er derfor kun bogstavet man anfører når man laver et mnemonic.
Hvis vi vender tilbage til eksemplet med en file-menu, vil den med de normale shortcuts få følgende udseende:
Figur 9:
Mnemonics i menu
Menuen laves med følgende implementation af makeMenuBar:
Source 9:
Angivelse af mnemonics
private JMenuBar makeMenuBar() {
  JMenuBar bar = new JMenuBar();
  
  JMenu fileMenu = new JMenu( "File" );
  fileMenu.setMnemonic( 'F' );

    newMI = new JMenuItem( "New" );
    newMI.setMnemonic( 'N' );
    newMI.addActionListener( this );
    fileMenu.add( newMI );
  
    openMI = new JMenuItem( "Open" );
    openMI.setMnemonic( 'O' );
    openMI.addActionListener( this );
    fileMenu.add( openMI );
  
    closeMI = new JMenuItem( "Close" );
    closeMI.setMnemonic( 'C' );
    closeMI.addActionListener( this );
    fileMenu.add( closeMI );
  
    fileMenu.addSeparator();
  
    saveMI = new JMenuItem( "Save" );
    saveMI.setMnemonic( 'S' );
    saveMI.addActionListener( this );
    fileMenu.add( saveMI );
  
  JMenu helpMenu = new JMenu( "Help" );
  
  bar.add( fileMenu );
  bar.add( helpMenu );
  
  return bar;
}
Man bemærker at menu-punkterne får underlinieret det tegn, som svarer til mnemonic. Hvis man vælger et mnemonic der ikke findes i menu-punktets tekst, kommer der ingen underliniering, men mnemonic virker alligevel.

 

5.2 Accelerators

Acceleratorer kræver ikke at de gøres "synlige", ved at man klikker sig frem til dem via en række mnemonics. Selvom ingen menu er åbnet kan man blot anvende taste-kombinationen, og menu-punktet bliver aktiveret. Man kan af samme grund ikke knytte en accelerator til en menu, men kun til egentlige menu-punkter.
Hvis vi igen anvender eksemplet med file-menuen, kunne vi for eksemplets skyld lave følgende shortcuts, vha. accelerators:
Figur 10:
Accelerators i menu
Alt, Ctrl eller Shift Som man ser kan disse shortcuts laves med enkelte taster, f.eks. 'N', eller en eller flere af tasterne Alt, Ctrl eller Shift, som kan kombineres frit. Ved at anvende Shift gør man shortcut'et case-sensitivt.
Menuen laves med følgende implementation af makeMenuBar:
Source 10:
Angivelse af accelerators
private JMenuBar makeMenuBar() {
  JMenuBar bar = new JMenuBar();
  
  JMenu fileMenu = new JMenu( "File" );
  fileMenu.setMnemonic( 'F' );

    newMI = new JMenuItem( "New" );
    newMI.setAccelerator( KeyStroke.getKeyStroke( 'N', 0 ) );
    newMI.addActionListener( this );
    fileMenu.add( newMI );
  
    openMI = new JMenuItem( "Open" );
    openMI.setAccelerator( KeyStroke.getKeyStroke( 'O', Event.CTRL_MASK ) );
    openMI.addActionListener( this );
    fileMenu.add( openMI );
  
    closeMI = new JMenuItem( "Close" );
    closeMI.setAccelerator(
      KeyStroke.getKeyStroke( 'C', Event.SHIFT_MASK + Event.ALT_MASK ) );
    closeMI.addActionListener( this );
    fileMenu.add( closeMI );
  
    fileMenu.addSeparator();
  
    saveMI = new JMenuItem( "Save" );
    saveMI.setAccelerator(
      KeyStroke.getKeyStroke( 'S', Event.CTRL_MASK + Event.ALT_MASK ) );
    saveMI.addActionListener( this );
    fileMenu.add( saveMI );
  
  JMenu helpMenu = new JMenu( "Help" );
  
  bar.add( fileMenu );
  bar.add( helpMenu );
  
  return bar;
}
Til at sætte en accelerator for et menu-punkt kaldes metoden:
void setAccelerator( KeyStroke keyStroke )
Til at lave instansen af KeyStroke bruges følgende metode:
KeyStroke KeyStroke.getKeyStroke( int keyCode, int modifiers )
hvor keyCode er UniCode for det pågældende tegn og modifiers er supplerende egenskaber for "taste-trykket". Disse modifiers kan være en eller flere af følgende konstanter:
Event.SHIFT_MASK
Event.CTRL_MASK
Event.META_MASK
Event.ALT_MASK
Konstanterne er defineret som 1, 2, 4, 8 og de kan derfor kombineres ved simpel addition, som det er gjort i eksemplet ovenfor. Mig bekendte findes Meta-tasten ikke under Win32 (den findes under UNIX).