Layout Managere

Opgaver

 

 

Når man taler med begyndere, der laver GUI-programmring i Java, støder man ofte på den holdning, at det er grimt! De har problemer med at få de grafiske komponenter placeret præcist hvor de ønsker det, er jeg har set de mest utrolige konstruktioner, der skulle tvinge Java til at gøre som de ville. De problemer de støder på, har deres rod i manglende kendskab til layout-managere.
Der findes seks layout-managere:
Java's seks layout-managere
FlowLayout
BorderLayout
GridLayout
BoxLayout
CardLayout
GridBagLayout
Antallet gør, at mange vælger kun at lære om de første 2-3 stykker og dernæst begynde at lave applikationer. Det er katastrofalt!
Problemet ligger i den sidste layout-manager: GridBagLayout. Hvis man ikke lærer at bruge GridBagLayout kan man lige så godt glemme det - man får aldrig lavet en grafisk brugergrænseflade der er værd at se på. De fem første layout-managere er simple, mens GridBagLayout er mere kompliceret. Det bør ikke afholde én fra at lære at bruge den, for uden den, kan man lige så godt lade være med at beskræftige sig med GUI-programmering i Java!
GUI Buildere Nogle vælger et andet alternativ: GUI-Buildere. En GUI-Builder er et visuelt værktøj, hvor man kan opbygge grænsefladerne ved at placere de grafiske komponenter med musen og efterfølgende generere en file som indeholder GUI-koden. Denne file kan man så føje til sin applikation og anvende den. Dermed behøver man ikke beskæftige sig med layout-managere, da denne del af koden laves automatisk.
Det er udemærket med en GUI-Builder, men ...!
Hvis man skal kunne bruge det resultat en GUI-Builder genererer, skal man vide hvad det er den laver, ellers kan man ikke kæde det sammen med resten af programmet. Det er derfor bydende nødvendigt, at man først lærer at lave "rigtig" GUI-Programmering. Når man har lært det, kan man begynde at bruge GUI-Buildere, som kan være tidsbesparende og nyttige i prototyping.
Jeg har set flere projekter, hvor de studerende er stoppet efter designet af brugergrænsefladerne, med ordene: "Vi kunne desværre ikke nå at implementere nogen af grænsefladerne". Situationen er normalt den samme: De har brugt en GUI-Builder. Man sidder med tanken i baghovedet: "De har ikke implementeret noget, fordi de ikke kan forstå hvad det er GUI-Builderen har lavet og de aner ikke hvordan de skal få det til at fungere sammen med deres program".
Den mest katastrofale indlæringsproces man kan opleve i forbindelse Java og GUI-programmering er:

- Lær kun de mest simple layout-managere at kende
- Bliv irriteret over hvor "dumme" de er
- Slå alle layout-managere fra
- Placer alt manuelt med pixels nøjagtighed
- Bliv træt af den tid det tager (det tager en evighed at justere)
- Brug en GUI-Builder
- Bliv frustreret over ikke at kunne få det til at fungere sammen med sit program
- Begynde at programmere Visual Basic
- Lide hjernedøden :-)

Lær det ordentligt! Jeg har set ovenstående forløb utallige gange, og vil derfor kraftigt understrege vigtigheden af at lære GUI-programmering ordentlig!
Hvis man ikke har tid til, i første omgang, at lære samtlige layout-managere at kende, vil jeg anbefale at man starter med følgende tre layout-managere:
FlowLayout
BorderLayout
GridBagLayout
I særdeleshed skal man give sig tid til at studere den sidste, med mindre udseendet af éns brugergrænseflader er ligegyldigt (det kan det være, hvis man blot vil studere grafiske komponenter og event-håndtering)

 

1. Hvordan man "sætter" en layout manager

Når man ønsker at anvende en layout-manager i forbindelse med en container (typisk en JFrame eller et JPanel), gør man det samme uanset hvilken layout-manager man bruger. Derfor vil vi berøre dette, før vi ser på den første layout-manager.
Selve layout-manageren er en instans af en af de seks layout-klasser. Hvis vi f.eks. har et JPanel og ønsker at give det en FlowLayout-manager gøres det med:
 
JPanel panel = ...
  ...

panel.setLayout( new FlowLayout() );
Her er anvendt metoden setLayout:
void setLayout( LayoutManager manager )
der er erklæret i superklassen Container.
For en JFrame er det lidt anderledes. Her skal man ikke kalde setLayout på selve JFrame'n, men i stedet gør det på ContentPane. Hvis vi f.eks. vil sætte en JFrame til at bruge et FlowLayout gøres det typiske ved at placere følgende linie i konstruktoren:
 
getContentPane().setLayout( new FlowLayout() );
Man skal sætte layout-manageren før man begynder at tilføje komponenter til containeren.
JPanel og JFrame har hver deres default-layout. For JPanel er det FlowLayout, og for JFrame er det BorderLayout. Man behøver ikke sætte noget layout; hvis man er tilfreds med disse i forbindelse med JPanel henholdvis JFrame.

 

2. java.awt.FlowLayout

FlowLayout er default-layout for JPanel.

 

2.1 Design

FlowLayout er den mest enkle af de seks layout managere. Det "flow" der indgår i navnet, er den sammenhæng man finder i almindelig tekst på et stykke papir; hvor ordene kommer i en "lind strøm". Man skriver fra venstre mod højre, og når der ikke er mere plads skifter man til næste linie.
Det samme er tilfældet i FlowLayout. Her placeres de grafiske komponenter efter hinanden, som på en "linie", og man skifter til næste "linie" når det næste komponent ikke kan være inden for "rammen".
Det eneste man kan indstille i FlowLayout, er om komponenterne skal være venstre-, højre- eller midter-stillede. Default er midter-stillede komponenter.
Betragt følgende fire vinduer, der illustrerer default, og de tre indstillinger af det såkaldte alignment.
Figur 1:
De tre alignments
I vinduerne har vi placeret otte knapper med tiltagende størrelse.

 

2.2 Anvendelse

Man sætter alignment med følgende metode:
void setAlignment( int align )
på layout-manageren.
align skal være en af konstanterne:
FlowLayout.LEFT
FlowLayout.CENTER
FlowLayout.RIGHT
Man skal dog være opmærksom på at dette metode-kald alene, ikke ændrer noget. Der skal efterfølgende laves et kald af følgende metode (ligeledes på layout-manageren):
void layoutContainer( Container target )
hvor target er den container som layout-manageren er tilknyttet.
Følgende stykke kode, skitserer hvordan man kunne sætte et FlowLayout på en JFrame, med alignment til højre:
class FlowFrame extends JFrame ... {

  ...

  private FlowLayout layout;

  public FlowFrame( ... ) {
    super( ... );

    layout = new FlowLayout();
 
    getContentPane().setLayout( layout );

    ...
  }

  ...

    layout.setAlignment( FlowLayout.RIGHT );
    layout.layoutContainer( getContentPane() );    
 
  ...
}
I eksemplet indikeres det, at man sætter alignment (til højre) på et senere tidspunkt end ved udførelse af konstruktoren. Hvis man sætter alignment i forbindelse med konstruktoren, og før man tilføjer komponenter, er det ikke nødvendigt at kalde layoutContainer.

 

3. java.awt.BorderLayout

BorderLayout er default-layout for JFrame. Ønsker man derfor at anvende et BorderLayout i en JFrame, behøver man ikke sætte en layout manager.

 

3.1 Design

BorderLayout arbejder ikke med linier, men med områder. Den inddeler i fem felter, der er har forskellige egenskaber. De fire af felterne er opkaldt efter de fire verdenshjørner og det femte hedder CENTER. Denne to-deling af felterne finder man også i deres adfærd i forbindelse med komponenter. CENTER-feltet opfører sig på én måde og de fire andre felter på en anden måde.
Følgende vindue illustrerer opdelingen i de fem felter:
Figur 2:
De fem felter
Alle fem felter kan kun indeholde ét komponent. I eksemplet her, er der placeret en JButton, med det tilhørende navn, i hvert områder.
De fire verdenshjørner deles ikke om hjørnerne. Da felterne naturligvis er firkantede, er det geometrisk umuligt for dem at mødes i hjørnerne. Man har derfor vedtaget at hjørnerne tilhører NORTH og SOUTH.
De fire verdenshjørner vil pakke sig ud til siderne. De vil ikke brede sig ud på ledig plads. Al overskydende plads vil blive tildelt CENTER, der breder sig. Hvis man resizer overstående vindue så det f.eks. fylder hele skærmen vil WEST og EAST stadig have samme bredde som på figuren, og NORTH og SOUTH vil stadig have samme højde. CENTER vil derimod brede sig, så den fylder det meste af skærmen.
Denne tilbageholdenhed, der udvises fra verdenshjørnernes side er så dominerende, at de end ikke breder sig når CENTER ikke har noget komponent. Hvis vi fjerner knappen fra CENTER-feltet bliver billedet:
Figur 3:
Uden CENTER-felt
De fire verdenshjørner fylder nøjagtig det samme som før og CENTER-feltet står tomt tilbage.
Verdenshjørnerne breder sig kun på ledig plads, hvis denne ledige plads opstår ved at andre verdenshjørner mangler. Hvis f.eks. NORTH ikke indeholder noget komponent vil WEST, CENTER og EAST alle brede sig mod nord.
Hvis vi fjerner knappen fra NORTH-feltet, og tilføjer CENTER-knappen igen, bliver billedet:
Figur 4:
Uden NORTH-felt
Man ser at WEST og EAST nu breder sig ud til hjørnerne. Man bemærker også at det ville være geometrisk umuligt for CENTER-feltet af bemægtige sig disse hjørne-områder, da det ville kræve at WEST og EAST også blev fjernet.
Når CENTER-feltet kan tage det hele, bliver der ikke noget til de andre felter.
Hvis vi f.eks. fjernede WEST, og havde komponenter i alle andre felter, ville billedet være:
Figur 5:
Uden WEST-felt
Her har CENTER-feltet bredt sig, og taget den ledige plads.
Lad os se hvordan billedet bliver hvis vi fjerner flere af verdenshjørnerne. Hvis vi f.eks. fjerner EAST og SOUTH bliver billedet:
Figur 6:
Uden EAST- og SOUTH-felt
Igen er det alene CENTER-feltet der breder sig mens de to andre er uændrede.
For til slut er understrege verdenshjørnernes tilbageholdenhed vedrørende ledig plads, vil vi se billedet når kun WEST har et komponent:
Figur 7:
WEST-feltet alene
WEST står urokkelig pakket ud til venstre og ignorerer den ledige plads.

 

3.2 Anvendelse

Selve opbygningen og placeringen af de fem knapper i de fem felter laves med følgende linier, placeret i framens konstruktor:
getContentPane().setLayout( new BorderLayout() );

northButton  = new JButton( "NORTH" );
eastButton   = new JButton( "EAST" );
southButton  = new JButton( "SOUTH" );
westButton   = new JButton( "WEST" );
centerButton = new JButton( "CENTER" );

getContentPane().add( northButton,  BorderLayout.NORTH );
getContentPane().add( eastButton,   BorderLayout.EAST );
getContentPane().add( southButton,  BorderLayout.SOUTH );
getContentPane().add( westButton,   BorderLayout.WEST );
getContentPane().add( centerButton, BorderLayout.CENTER );
Det eneste nye er add-metoden med to parametre1, og de fem konstanter, som optræder som anden parameter:
void add( Component comp, Object constraints )
Som det ses er signaturen meget abstrakt. Ikke alene kan man tilføje et hvilket som helst komponent (også de gamle heavyweight komponenter), men den anden parameter kan være hvad som helst.
I tilfældet med BorderLayout er anden parameter en angivelse af, hvilket felt komponentet skal add'es til. De fem konstanter, der er vist ovenfor, angiver hver en af de fem felter, og der findes ikke andre. Interessant nok, er de fem konstanter tekststrengs-konstanter. De har samme indhold som navnet på verdenshjørnet. F.eks. er BorderLayout.WEST lig med "West", og man kunne i stedet vælge at bruge denne tekststreng, men bør lade være.
Default er BorderLayout.CENTER. Vi kunne derfor ændre add-kaldet ovenfor til:
getContentPane().add( centerButton );

 

4. java.awt.GridLayout

Idéen i GridLayout er at inddele det hele i rektangulære områder af samme størrelse og placere ét komponent i hvert. Som f.eks. følgende vindue med tolv JButton's.
Figur 8:
Tolv knapper i GridLayout
Brugergrænsefladen efterligner de grundlæggende taster på min telefon.
For at lave vinduet skal man angive hvor mange rækker og kolonner der skal være. Efterfølgende "hælder" man komponenterne i, i den rigtige rækkefølge, der følger læseretningen som vi kender den fra FlowLayout.
Konstruktoren til ovenstående frame har følgende indhold.
public GridFrame( String title ) {
  super( title );
  
  getContentPane().setLayout( new GridLayout( 4, 3 ) );
  
  getContentPane().add( new JButton( "1" ) );
  getContentPane().add( new JButton( "2" ) );
  getContentPane().add( new JButton( "3" ) );
  getContentPane().add( new JButton( "4" ) );
  getContentPane().add( new JButton( "5" ) );
  getContentPane().add( new JButton( "6" ) );
  getContentPane().add( new JButton( "7" ) );
  getContentPane().add( new JButton( "8" ) );
  getContentPane().add( new JButton( "9" ) );
  getContentPane().add( new JButton( "*" ) );
  getContentPane().add( new JButton( "0" ) );
  getContentPane().add( new JButton( "#" ) );
  
  setDefaultCloseOperation( EXIT_ON_CLOSE );
  
  pack();
  setVisible( true );
}
Selve indholdet af denne konstruktor understreger det monotone og ensartede i GridLayout.
GridLayout har følgende konstruktorer:
GridLayout()
GridLayout( int rows, int columns )
GridLayout( int rows, int columns, int hgap, int vgap )
rows og columns er naturligvis rækker og kolonner (rækker er "højden", kolonner er "bredden"). Default-konstruktoren svarer til et kald af den anden konstruktor med:
 
this( 1, 0 );
Højest én af de to parametre rows og columns kan være 0. Værdien 0 betyder, at der er "uendelig" mange pladser i den pågældende retning. F.eks. betyder this-kaldet ovenfor, at der er én række med uendelig mange kolonner.
hgap og vgap betyder: horizontal gap og vertical gap. De angiver i pixels, hvor meget padding der skal være mellem komponenterne.
Hvis vi i eksemplet med telefonen havde anvendt hgap 5 og vgap 10 ville vinduet have fået følgende udseende:
Figur 9:
Tolv knapper med hgap og vgap
Bemærk, at der ikke kommer noget "gap" mellem de grafiske komponenter og containerens ydre grænser.

 

5. javax.swing.BoxLayout

 

5.1 Design

  BoxLayout er et relativ simpelt layout; hvis grundidé er at placere en række grafiske komponenter i enten en vandret række:
Figur 10:
Vandret række af grafiske komponenter
  eller en lodret række:
Figur 11:
Lodret række af grafiske komponenter
Som man måske kan se af eksemplet med knapperne, der er placeret lodret, vil BoxLayout ikke styre hvor meget de enkelte komponenter fylder. BoxLayout prøver at give hvert komponent, netop den plads det selv ønsker - derfor den forskellige bredde af knapperne (knapperne første og anden er en anelse breddere end de tre andre knapper). Af samme grund bliver knapperne heller ikke bredt ud over den ledige plads i højre side af framen.
Swing anvender selv BoxLayout (i form af en subklasse til BoxLayout) til at ordne menupunkter på popup og almindelige menuer.

 

5.2 Anvendelse

Om de grafiske komponenter skal ordnes vandret eller lodret, angives som parameter til konstruktoren:
BoxLayout( Container target, int axis )
idet man som axis bruger en af følgende to konstanter:
BoxLayout.X_AXIS
BoxLayout.Y_AXIS
F.eks. kan man lave eksemplet, fra figur 11, på følgende måde:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

class BoxLayoutFrame extends JFrame {

  public BoxLayoutFrame( String title ) {
    super( title );
    
    BoxLayout layout = new BoxLayout( getContentPane(), BoxLayout.Y_AXIS );
    getContentPane().setLayout( layout );
    
    String[] nummer = { "første", "anden", "tredie", "fjerde", "femte" };
    
    for ( int i=0; i<nummer.length; i++ )
      getContentPane().add( new JButton( nummer[i] ) );
  
    setDefaultCloseOperation( EXIT_ON_CLOSE );
  
    pack();
    setVisible( true );
  }
}

 

5.3 javax.swing.Box

Box-klassen er en hjælpeklasse, der kan være nyttig når man arbejder med BoxLayout. Box er grundlæggende et JComponent, der er født med et BoxLayout, og man kan bruge den som sådan. Ud over, at instanser af Box er grafiske komponenter, har klassen en række statiske fabriks-metoder, der kan være nyttige i forbindelse med BoxLayout, og dermed ikke kun i forbindelse med instanser af Box:
 

 

6. java.awt.CardLayout

 

6.1 Design

Den grundlæggende idé i CardLayout er, at man har en bunke kort, hvoraf man altid kun kan se det øverste.
Man vælger som oftest at lade disse kort være instanser af JPanel. Hvert panel er en brugergrænseflade og ved at samle dem i en container der anvender et CardLayout, bliver det muligt at skifte hurtigt mellem de forskellige grænseflader.
Når man arbejder med kortene kan man gøre det enten med iterator-metoder eller med absolut reference til hvert kort. Man kan også blande de to teknikker, da de ikke udelukker hinanden.
Vi vil i det følgende opdele gennemgange efter disse to tilgangsvinkler til kortene.

 

6.2 Absolut reference

Betragt følgende eksempel:
Figur x:
Vindue med CardLayout
Layoutet i dette vindue er opbygget med fem knapper i et GridLayout, placeret i WEST i et BorderLayout. I CENTER af dette BorderLayout har vi placeret et JPanel med et CardLayout og fem kort, der hver indeholder et JLabel. Panelet i CENTER har en EmptyBorder på 50 pixels, som padding.
Eksemplet er lavet med følgende frame:
class CardFrame extends JFrame implements ActionListener {
  
  private CardLayout layout;
  private JPanel panel;
  
  public CardFrame( String title ) {
    super( title );
    
    getContentPane().setLayout( new BorderLayout() );
    
    JPanel knapper = new JPanel();
    knapper.setLayout( new GridLayout( 0, 1 ) );

    panel = new JPanel();
    layout = new CardLayout();
    panel.setLayout( layout );
    panel.setBorder( new EmptyBorder( 50, 50, 50, 50 ) );
    
    String[] numre = { "Første", "Anden", "Tredie", "Fjerde", "Femte" };
    
    for ( int i=0; i<numre.length; i++ ) {
      String n = "" + (i+1);
      JLabel label = new JLabel( numre[i] );

      JButton b = new JButton( n );
      b.addActionListener( this );

      knapper.add( b );
      panel.add( label, n );
    }

    getContentPane().add( panel, BorderLayout.CENTER );
    getContentPane().add( knapper, BorderLayout.WEST );

    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    pack();
    setVisible( true );
  }
  
  public void actionPerformed( ActionEvent e ) {
    Object source = e.getSource();
    
    if ( source instanceof JButton ) {
      JButton b = (JButton) source;
      layout.show( panel, b.getText() );
    }
  }
}
I kildeteksten er markeret en række linier. Det er dem der vedrører selve CardLayout.
Først giver vi panel et CardLayout. Dernæst laver vi i for-sætningen fem labels, der alle add'es til panel. Hver af dem er et kort, som vi add'er sammen med en anden parameter. Denne parameter er en teksstreng, som bruges som senere reference til kortet.
I actionPerformed henter vi knappens tekst (som vi bekvemt nok brugte som tekststreng til senere reference) og beder layoutet om at vise det tilsvarende kort med metoden:
void show( Container container, String name )
Ved hjælpe af denne metode, kan vi på et hvilket som helst tidspunkt få vist et givent kort, blot vi har reference-navnet. Som første parameter angiver man den container som CardLayout er sat til.
Følgende figur illustrerer hvordan man med knapperne kan vælge hvilket kort der vises:
Figur x:
Sammenhæng mellem knapper og kort

 

6.3 Iterator-metoder

Betragt følgende eksempel:
Figur x:
Vindue med CardLayout
Layoutet i dette vindue er opbygget på samme måde som i foregående eksempel. Knapperne i WEST er ændret så de svarer til de fire iterator-metoder der findes i CardLayout.
Eksemplet er lavet med følgende frame:
class CardFrame extends JFrame implements ActionListener {
  
  private CardLayout layout;
  private JPanel panel;
  private JButton firstB, nextB, prevB, lastB;
  
  public CardFrame( String title ) {
    super( title );
    
    getContentPane().setLayout( new BorderLayout() );
    
    JPanel knapper = new JPanel();
    knapper.setLayout( new GridLayout( 0, 1 ) );

    panel = new JPanel();
    layout = new CardLayout();
    panel.setLayout( layout );
    panel.setBorder( new EmptyBorder( 50, 50, 50, 50 ) );
    
    String[] numre = { "Første", "Anden", "Tredie", "Fjerde", "Femte" };
    
    for ( int i=0; i<numre.length; i++ ) {
      String n = "" + (i+1);
      JLabel label = new JLabel( numre[i] );

      panel.add( label, n );
    }
    
    firstB  = new JButton( "Første" );
    firstB.addActionListener( this );
    knapper.add( firstB );

    nextB   = new JButton( "Næste" );
    nextB.addActionListener( this );
    knapper.add( nextB );

    prevB   = new JButton( "Forrige" );
    prevB.addActionListener( this );
    knapper.add( prevB );

    lastB = new JButton( "Sidste" );
    lastB.addActionListener( this );
    knapper.add( lastB );

    getContentPane().add( panel, BorderLayout.CENTER );
    getContentPane().add( knapper, BorderLayout.WEST );

    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    pack();
    setVisible( true );
  }
  
  public void actionPerformed( ActionEvent e ) {
    Object source = e.getSource();
    
    if ( source == firstB )
      layout.first( panel );
    else if ( source == nextB )
      layout.next( panel );
    else if ( source == prevB )
      layout.previous( panel );
    else if ( source == lastB )
      layout.last( panel );
  }
}
Man ser i actionPerformed hvorledes de fire knapper svarer til hver deres iterator-metode.
Følgende figur illustrerer hvordan man med knapperne kan iterere kortene:
Figur x:
Iterering af kort med iterator-metoder

 

7. java.awt.GridBagLayout

GridBagLayout er, med flere længder, den mest komplekse af de seks layout-managere. Vi vil derfor studere den på en lidt anden måde end de foregående. Først vil vi se et eksempel på hvordan den kan anvendes og dernæst vil vi gennemgå de enkelte detaljer i GridBagLayout hver for sig.

 

7.1 Eksempel

Følgende eksempel illustrerer hvordan man arbejder sig frem mod at realisere det design man ønsker vha. GridBagLayout. Eksemplet er rimelig enkelt og vi berører ikke alle mulighederne i GridBagLayout.
Når man vil opbygge en brugergrænseflade, er det naturligvis altid nyttigt at tegne den. I forbindelse med GridBagLayout er det endvidere nyttigt at gøre det på et stykke papir. Man kan enten bruge pen og papir, eller et GUI-designprogram og lave en udskrift. Jeg har valgt at lave en tegning i hånden:
Figur x:
Vores design på papir
Jeg har valgt at lave en frame med fem grafiske komponenter: et JTextField, et JTextArea og tre JButton's. På tegningen har jeg valgt at bruge disse navne, da eksemplet er meget generelt. Ellers ville man normalt beskrive indhold/funktion.
På figuren er der indtegnet en række linier med rødt. Disse linier viser de rækker og kolonner, der beskriver komponenternes placering relativt i forhold til hinanden. Det første man gør er at fastlægge disse linier og dermed beskrive placering og udstrækning af de enkelt komponenter.
Ud fra linierne kan vi se at JTextField skal være to bred og JTextArea skal være tre høj. Bortset fra disse relative størrelser skal alle andre dimensioner være én række eller kolonne.
Placeringerne angives med koordinaten for det øverste venstre hjørne af komponentet.
Der er mange andre attributter der skal indstilles, før det kommer til at ligne, men vi vil i første omgang holde os til disse.
I GridBagLayout skal der disse egenskaber knyttes til det enkelte komponent. Det gøres ved at lave et objekt for hvert komponent, der beskriver dets visuelle egenskaber i relation til layoutet. Objektet er en instans af GridBagConstraints, og det indeholder attributterne.
Der er fire attributter i denne klasse, som omhandler placering og udtrækning af det grafiske komponent. Placeringen angives i gridx og gridy, udstrækningen i gridwidth og gridheight.
Det GridBagConstraints-objekt vi skal bruge i forbindelse med JTextField kunne vi f.eks. lave og effektuere med følgende kode:
private GridBagLayout layout;
private JTextField tekstFelt;
              
...
              
layout = new GridBagLayout();

getContentPane().setLayout( layout );

...

GridBagConstraints con = new GridBagConstraints();

tekstFelt = new JTextField( "JTextField" );
con.gridx = 0;      // positionens x-koordinat
con.gridy = 0;      // positionens y-koordinat
con.gridwidth = 2;  // udstrækning i bredden
con.gridheight = 1; // udstrækning i højden

layout.setConstraints( tekstFelt, con );
getContentPane().add( tekstFelt );
Først laver vi naturligvis selve GridBagLayout og sætter det. Dernæst laver vi en instans af GridBagConstraints og JTextField. Vi sætter de fire af GridBagConstraints attributter, der beskriver placering og udstrækning af komponentet. Inden vi add'er komponentet, foretager vi et kald af setConstraints på layout-manageren. Dette kald knytter komponentet og GridBagConstraints-objektet sammen i layout-manageren, så den anvender det når den skal vise komponentet.
Som man kan se er det mange linier for et komponent, og når vi senere arbejder med flere af attributterne bliver kun værre. Man kan evt. lave en eller flere service-metoder, der forenkler det lidt (Vi skal senere se, hvordan det kan gøres endnu bedre, ved at bruge delegering). Vi vil indføre to sådanne service-metoder.
Den første knytter sig til instantieringen af GridBagConstraints-objektet. Det er forsekelligt hvilke af attributterne man ønsker at anvende, men netop de fire vi bruger her, ønsker man altid at sætter. Derfor vil vi lave en fabriksmetode, der returnerer en instans af GridBagConstraints, hvor de fire attributter sætter til værdien af fire parametre til metoden. Vi kalder metoden createGBC:
private GridBagConstraints createGBC( int x, int y, int width, int height ) {
  GridBagConstraints gbc = new GridBagConstraints();
  
  gbc.gridx = x;
  gbc.gridy = y;
  
  gbc.gridwidth = width;
  gbc.gridheight = height;
  
  return gbc;
}
Når vi får objektet tilbage fra metoden, kan vi vælge at sætte andre af attributterne, alt efter den konkrete situation.
I forbindelse med den afsluttende indstilling vil vi også anvende en service-metode add:
private void add( JComponent component, GridBagConstraints gbc ) {
  layout.setConstraints( component, gbc );
  getContentPane().add( component );
}
Denne metode kræver at layout er en instansvariabel. Man kunne alternativt lade den være en parameter til metoden, men det synes unødvendigt da man normalt alligevel lader layout være en instansvariabel med henblik på eventuelle senere justeringer.
Ved anvendelse af disse to servicemetoder bliver eksemplet med JTextField i stedet:
private GridBagLayout layout;
private JTextField tekstFelt;
              
...
              
layout = new GridBagLayout();

getContentPane().setLayout( layout );

...

GridBagConstraints con;

tekstFelt = new JTextField( "JTextField" );
con = createGBC( 0, 0, 2, 1 );
add( tekstFelt, con );
Det har givet en kraftig reduktion i antallet af linier. Når vi senere føjer flere attributter til, vil det igen begynde at vokse i omfang, men vi har taget toppen af.
Da vi i dette eksempel kun vil fokusere på layoutet og ikke se på event-håndteringen, vælger vi i det følgende at flytte instantieringen af widgets ind i selve kaldet af add, men i det omfang man har brug for at huske disse widgets, skal der naturligvis laves referencer til dem.
Den samlede konstruktor med samtlige komponenter bliver:
public GridBagFrame( String title ) {
  super( title );
    
  layout = new GridBagLayout();
    
  getContentPane().setLayout( layout );
  
  GridBagConstraints con;
    
  con = createGBC( 0, 0, 2, 1 );
  add( new JTextField( "JTextField" ), con );
    
  con = createGBC( 0, 1, 1, 3 );
  add( new JTextArea( "JTextArea" ), con );
    
  con = createGBC( 1, 1, 1, 1 );
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 2, 1, 1 );
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 3, 1, 1 );
  add( new JButton( "JButton" ), con );
    
  setDefaultCloseOperation( EXIT_ON_CLOSE );
    
  pack();
  setVisible( true );
}
Vinduet får følgende udseende:
Figur x:
Med placering og udstrækning
Måske lidt skuffende. Det ligger stadig langt fra det ønskede design, og vi skal have sat en del andre attributer før vi når målet.
JTextField og JTextArea antager minimale størrelser. Det første man kunne ønske sig, var at få dem til at brede sig ud på den plads de har til rådighed.
Komponenters tilbøjeligheden til at resize så de fylder deres plads ud, sættes med attributten fill.
Man kan sætte denne attribut til en af følgende fire konstanter:
GridBagConstraints.NONE         (default)
GridBagConstraints.HORIZONTAL
GridBagConstraints.VERTICAL
GridBagConstraints.BOTH
Default værdien NONE, betyder at komponentet ikke vil brede sig. Det er resultatet af denne default-værdi vi ser ovenfor. HORIZONTAL betyder at den vil brede sig i bredden, mens VERTICAL betyder at den breder sig i højden. BOTH betyder at den vil brede sig i begge retninger.
Vi kan nu tage stilling til hvordan vi ønsker komponenterne skal brede sig i deres områder.
JTextField skal brede sig i bredden, og selvom den nok aldrig vil blive højere (den vil altid kun være en tekstlinie), vælger vi alligevel at være præcise, og bruger derfor HORIZONTAL.
Alle andre komponenter ønsker vi skal brede sig i alle retninger, og vi vælger derfor BOTH til dem.
Konstruktoren får nu følgende indhold:
public GridBagFrame( String title ) {
  super( title );
    
  layout = new GridBagLayout();
    
  getContentPane().setLayout( layout );
    
  GridBagConstraints con;
    
  con = createGBC( 0, 0, 2, 1 );
  con.fill = GridBagConstraints.HORIZONTAL;
  add( new JTextField( "JTextField" ), con );
    
  con = createGBC( 0, 1, 1, 3 );
  con.fill = GridBagConstraints.BOTH;
  add( new JTextArea( "JTextArea" ), con );
    
  con = createGBC( 1, 1, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 2, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 3, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  add( new JButton( "JButton" ), con );
    
  setDefaultCloseOperation( EXIT_ON_CLOSE );
    
  pack();
  setVisible( true );
}
Og vinduet får følgende udseende:
Figur x:
Med fill
Det hjælper lidt, men det hele er for småt. Specielt er JTextArea helt forkert.
Problemet med JTextArea kan vi i første række løse ved at give det en størrelse med:
 
add( new JTextArea( "JTextArea", 8, 16 ), con );
Dermed få vinduet følgende udseende:
Figur x:
Med størrelse på JTextArea
Det begynder at ligne, men nu er der opstået et problem med den nederste af de tre knapper.
Pga. BOTH breder den sig på den ledige plads, og af en eller anden grund (se senere) er det netop den nederste knap, der får al den ledige plads.
Ifølge vores design skal de tre kanpper deles ligeligt om pladsen. Vi skal have indstillet hvor meget komponenterne fylder forholdsmæssigt i forhold til hinanden. Til dette formål skal vi bruge attributterne weightx og weighty.
Defaultværdien for disse attributter er 0. 0 betyder at komponentet i den givne retning x: vandret, y: lodret, får den størrelse, der er dens minimale udstrækning. Hvis alle komponenter i en retning lodret/vandret har attributten sat til 0, vil det sidste komponent få den resterende plads. Det er derfor den nederste knap få al den ledige plads.
Hvad vis værdien ikke er 0? Layout-manageren vil tage alle attributter i en retning og summere dem. Dernæst vil den bruge deres andel af denne sum som forholdtal, når den lader komponenterne brede sig. Vi vil derfor sætte weighty for de tre knapper til den samme værdi, og lade weighty for JTextField forblive 0, da den ikke skal brede sig lodret (hvilket den alligevel ikke kan!).
Hvad skal vi konkret sætte weighty til? de to attributter weightx og weighty er double's, og deres konkrete værdier er uden betydning, det er forholdet mellem dem der er afgørende. Vi vil vælge at sætte de tre knappers weighty til 1.
Konstruktoren får nu følgende indhold:
public GridBagFrame( String title ) {
  super( title );
    
  layout = new GridBagLayout();
    
  getContentPane().setLayout( layout );
    
  GridBagConstraints con;
    
  con = createGBC( 0, 0, 2, 1 );
  con.fill = GridBagConstraints.HORIZONTAL;
  add( new JTextField( "JTextField" ), con );
    
  con = createGBC( 0, 1, 1, 3 );
  con.fill = GridBagConstraints.BOTH;
  add( new JTextArea( "JTextArea", 8, 16 ), con );
    
  con = createGBC( 1, 1, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  con.weighty = 1;
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 2, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  con.weighty = 1;
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 3, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  con.weighty = 1;
  add( new JButton( "JButton" ), con );
    
  setDefaultCloseOperation( EXIT_ON_CLOSE );
    
  pack();
  setVisible( true );
}
Og vinduet får følgende udseende:
Figur x:
Med weighty sat på de tre knapper
Hvis man trækker lidt i vinduet, for at gøre det større, vil man opdage følgende:
Figur x:
Vinduet efter det er gjort større af brugeren
Lodret ser det rimelig fornuftigt ud. De tre knapper har bredt sig på den ledige plads, og fordelt den ligeligt mellem dem. Derimod er der problemer vandret. Vi skal have sat weightx, så komponenterne kan fordele den ledige plads mellem dem. Ifølge designet skal størrelsesforholdet mellem knapperne og JTextArea være 7:2. Vi vælger derfor disse værdier til weightx. JTextAreas sættes til 7 og de tre knappers sættes til 2.
Det giver følgende udseende, hvis man resizer vinduet til samme størrelse som ovenfor:
Figur x:
Med anvendelse af weightx
Vores vindue er nu mere robust, idet det kan tåle, at man resizer.
Luft En sidste ting vi vil berøre er "luft". I forhold til vores ønskede design står komponenterne skulder ved skulder - der er ikke nogen luft imellem dem.
Luft laves ved at man sætter attributten insets til en instans af klassen: Insets.
Insets har selv fire attributter der angiver hvor meget luft (i pixels) der skal være i hver af de fire retninger. Insets konstruktor har følgende signatur:
Insets( int top, int left, int botton, int right )
For at få en ensartet fordeling af luften imellem komponenterne anvender vi den samme Insets til alle fem komponenter, med 5 pixels i hver retning.
public GridBagFrame( String title ) {
  super( title );
    
  layout = new GridBagLayout();
    
  getContentPane().setLayout( layout );
    
  GridBagConstraints con;
    
  Insets ins = new Insets( 5, 5, 5, 5 );
    
  con = createGBC( 0, 0, 2, 1 );
  con.fill = GridBagConstraints.HORIZONTAL;
  con.insets = ins;
  add( new JTextField( "JTextField" ), con );
    
  con = createGBC( 0, 1, 1, 3 );
  con.fill = GridBagConstraints.BOTH;
  con.insets = ins;
  con.weightx = 7;
  add( new JTextArea( "JTextArea", 8, 16 ), con );
    
  con = createGBC( 1, 1, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  con.insets = ins;
  con.weightx = 2;
  con.weighty = 1;
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 2, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  con.insets = ins;
  con.weightx = 2;
  con.weighty = 1;
  add( new JButton( "JButton" ), con );
    
  con = createGBC( 1, 3, 1, 1 );
  con.fill = GridBagConstraints.BOTH;
  con.insets = ins;
  con.weightx = 2;
  con.weighty = 1;
  add( new JButton( "JButton" ), con );
    
  setDefaultCloseOperation( EXIT_ON_CLOSE );
    
  pack();
  setVisible( true );
}
Det endelige vindue bliver:
Figur x:
Med anvendelse af insets

 

7.2 GridBagConstraints attributter

Vi vil her gennemgå de enkelte attributter og deres betydning.

 

7.2.1 gridx og gridy

Angiver placeringen af komponentet. Angivelsen er af øverste venstre hjørne af komponentet, og disse værdier starter fra ( 0, 0 ). Værdierne er ikke ækvidistante, men er relative placeringer.
Man kan evt. tænke på dem som index-lignende positioner som de kendes fra arrays.

 

7.2.2. gridwidth og gridheight

Angiver hvor mange kolonner henholdsvis rækker komponentet har til sin rådighed. Komponenten vil ikke nødvendigvis brede sig over det angivne område, da det er op til andre attributter at styre dette.
Området er beskyttet mod at andre komponenter trænger i ind på. Hvis komponentet ikke breder sig over hele området, henstår det resterende tomt.
Angivelsen af bredde/højde er ikke ækvidistant, da forskellige kolonner/rækker kan have forskellig bredde/højde.

 

7.2.3 weightx og weighty

Disse værdier er doubles, der fungerer som forholdstal. De beskriver den vægt hvormed de skal tildeles overskydende plads i konkurrence med andre komponenter i den pågældende retning.
weightx er vandt, og weighty er lodret.
Default er 0, og betyder at komponentet ikke breder sig, men antager sin minimale størrelse i den pågældende retning.
Når layout-manageren skal tildele overskydende plads beregner den summen af vægtene for de indgående komponenter, i den pågældende retning, og fordeler pladsen efter den del som deres vægt udgør af denne sum.

 

7.2.4 ipadx og ipady

Dette er de interne paddings (i modsætning til de externe, der beskrives nedenfor). De interne paddings angiver hvor meget komponentet vil lægge til sin minimale størrelse i den pågældende retning. Komponentet vil altid antage sin minimale størrelse plus disse paddings.
Effekten er derfor, at man udvider komponentets minimale størrelse i samspillet med de andre komponenter, så det altid er sikret denne ekstra plads inde i det område, hvor det befinder sig.
Default er ingen intern padding.

 

7.2.5 fill

Angiver om et komponent vil brede sig for at udfylde ledig plads i sit område. Attributten tildeles en af følgende fire konstanter.
GridBagConstraints.NONE         (default)
GridBagConstraints.HORIZONTAL
GridBagConstraints.VERTICAL
GridBagConstraints.BOTH
Default værdien NONE, betyder at komponentet ikke vil brede sig. Det er resultatet af denne default-værdi vi ser ovenfor. HORIZONTAL betyder at den vil brede sig i bredden, mens VERTICAL betyder at den breder sig i højden. BOTH betyder at den vil brede sig i begge retninger.

 

7.2.6 insets

insets angiver extern padding. Denne attribut sættes til en instans af Insets, som har følgende konstruktor:
Insets( int top, int left, int botton, int right )
De fire værdier angiver i pixels hvor meget extra plads (luft) der skal være mellem dette komponent (uanset dets udstrækning) og de tilstødende komponenter. To komponenter der støder op til hinanden, deles ikke om extern padding, og deres externe padding vil derfor blive placeret side om side.
Default er ingen extern padding.

 

7.2.7 anchor

Angiver den relative placering af et komponent i dets område, når den ikke udfylder det. Attributten sættes til en af følgende konstanter:
GridBagConstraints.CENTER
GridBagConstraints.NORTH
GridBagConstraints.NORTHEAST
GridBagConstraints.EAST
GridBagConstraints.SOUTHEAST
GridBagConstraints.SOUTH
GridBagConstraints.SOUTHWEST
GridBagConstraints.WEST
GridBagConstraints.NORTHWEST
Default er CENTER som placerer komponentet midt i dets område. De andre angivelser placerer komponentet ud til kanten af området i den pågældende retning.

 

7.3 GridBagGUI

Vi så ovenfor hvordan man kunne lette anvendelse af GridBagLayout med to service-metoder. Når man skal anvende GridBagLayout i større sammenhæng er det dog nyttigt at vælge en mere skalerbar løsning, der til gengæld kræver en større forståelse af objektorienteret design. Vi vil i dette afsnit studere et sådant design som jeg selv plejer at bruge i forbindelse med GridBagLayout.
Det grundlæggende skaleringsproblem ligger i, at der skal være flere/mange af de to service-metoder. Hvis man opbygger en mere sammensat GUI, med f.eks. flere paneler til at styrre de grafiske komponenter, er man nød til at lave en subklasse for hvert panel og indføre de to service-metoder i hver af dem. Det skyldes at metoderne anvender instansvariable i objektet og derfor er uløselig bundet til klassens layout.
Den designmæssige løsning ligger netop i at løsne metoderne fra selve klassen og placere dem i et andet objekt som får ansvaret for at håndtere layoutet. Lad os for eksemplet skyld antage at vi ønsker at anvende et GridBagLayout i forbindelse med en JFrame.
I første række ser vi derfor designet som en delegering af layout-opgaven til et andet objekt - en instans af en klasse vi selv laver: GridBagGUI.
Set fra JFrame ønsker vi at kunne sende grafiske komponenter til GridBagGUI og få dén til at placere dem i et GridBagLayout med de indstillinger af GridBagConstraints som vi også meddeler GridBagGUI. Det betyder at GridBagGUI skal have en række metoder til indstilling af layoutet samt en add-metode til de grafiske komponenter.
Hvad skal GridBagGUI have adgang til for kunne håndtere layout i JFrame'n?
Den skal blot have adgang til den container som de grafiske elementer føjes til. For en JFrame's vedkommende er det, det objekt, man får en reference til ved at kalde getContentPane, mens det for de fleste andre komponenter er komponentet selv - f.eks. er JPanel sin egen container.
De komponenter, der kan indeholde andre grafiske komponenter - dvs. grafiske containere - har alle en fælles superklasse, som passende hedder Container. Det er derfor GridBagGUI's konstruktor tager en Container som parameter:
public GridBagGUI( Container container ) {
  this.container = container;

  layout = new GridBagLayout();
  container.setLayout( layout );
    
  reset();
}
Konstruktoren gemmer naturligvis denne reference til senere add-kald, men mere interessant er det, at den selv laver en instans af GridBagLayout og sætter den som layout på containeren. GridBagGUI har nu de to objekter den skal bruge: Containeren og layoutet. (reset-metoden vender vi tilbage til senere).
Hvis der er tale om en JFrame vil vi instantiere den tilhørende GridBagGUI med kaldet:
GirdBagGUI i JFrame
... = new GridBagGUI( getContentPane() );
Mens man i et JPanel vil bruge:
GirdBagGUI i JPanel
... = new GridBagGUI( this );
 
Den vigtigste metode i GridBagGUI er add-metoden:
public void add( Component component ) {
  layout.setConstraints( component, gbc );
  container.add( component );
}

Her anvendes begge de associerede objekter: layout og container, og man genkender kaldene fra service-metoden af samme navn, som vi har anvendt tidligere.

 

Ser man på kildeteksten til GridBagGUI (hvilket vi ikke vil gøre her) er den præget af de mange metoder til indstilling af diverse GridBagConstraints.

Da jeg som nævnt selv anvender klassen i praksis, er der visse af metoderne, som ikke er helt pædagogisk korrekte, men jeg har alligevel taget dem med i det følgende:

 

Til indstilling af position og relativ størrelse er der følgende seks metoder:
void setPosition( int x, int y )
void setPositionX( int x )
void setPositionY( int y )

void setSize( int width, int height )
void setWidth( int width )
void setHeight( int height )

 

Til indstilling af fill er der hele tre metoder:
void setFill( int con )
void setFill( char con )
void setFill( String con )
Den første tager som ventet en af de sædvanlige GridBagConstraints-konstanter, mens de to andre er bekvemmelige. Man kan i stedet anføre en tekststreng: "HORIZONTAL", "VERTICAL", "BOTH" eller "NONE", eller forkorte det til et tegn: 'H', 'V', 'B' eller 'N'.

Som nævnt er det bekvemt. Man skal til gengæld ikke forvente at ens kode bliver nemmere at læse, specielt ikke for andre, og i særdeleshed ikke hvis man anvender enkelt bogstaver.

 

Dernæst er der tre metoder til indstilling af weightx og weighty:
void setWeight( int weightx, int weighty )
void setWeightX( int weight )
void setWeightY( int weight )
og to metoder til insets:
void setInsets( Insets insets )
void setInsets( int top, int left, int botton, int right )
og tre metoder til intern padding:
void setIPad( int padx, int pady )
void setIPadX( int pad )
void setIPadY( int pad )

 

Til slut er der to metoder til anchor; hvoraf den sidste får forkortelserne i setFill-metoderne til at blegne:
void setAnchor( int dir )
void setAnchor( String dir )
Her kan man nemlig anvende en af følgende forkortelser for de mulige GridBagConstraints: "C", "N", "NE", "E", "SE", "S", "SW", "W" eller "NW". Bemærk at "C" står for CENTER.
 
Endelig er der en reset-metode, der sætter alle GridBagConstraints til deres default-værdier:
void reset()

 

Vi vil ikke gennemgå et samlet eksempel på en anvendelse af GridBagGUI, men blot se et enkelt eksempel, nemlig følgende panel:
Der er lavet med følgende kode:
GridBagGUI gui = new GridBagGUI( this );

gui.setPosition( 0, 0 );
gui.setSize( 1, 1 );
gui.setFill( 'H' );
gui.setWeightX( 1 );
gui.add( input );

gui.setPosition( 1, 0 );
gui.setWeightX( 0 );
gui.add( searchButton );

gui.setPosition( 2, 0 );
gui.add( stopButton );
Som man ser drager jeg nytte af at genbruge de tidligere indstillinger af GridBagConstraints efterhånden som de enkelte grafiske komponenter føjes til, ved ikke at kalde reset, men i stedet ændre dem der skal være anderledes. Det gør koden kortere, men nedsætter til gengæld læsbarheden.
 

Kildetekst:

GridBagGUI.zip

 

 

8. JPanel

JPanel er naturligvis ikke nogen layout-manager, men den spiller en væsentlig rolle i forbindelse med opbygningen af grafiske brugergrænseflader med layout-managere.
Et JPanel er en ren container. Den kan indeholde grafiske komponenter og den har sig egen layout-manager, men den har ikke nogen visuel repræsentation i sig selv.
JPanels kan indeholde JPanels og på den måde optræder den i et Composite Pattern, på samme måde som f.eks. JMenu.
<Det skal der skrives en hel del mere om>

fodnoter:

 

1 Der findes i alt fem forskellige add-metoder i klassen Container.