Dialogboxe

Verden er fuld af dialogboxe. Små vinduer der springer frem på skærmen, med meddelelser eller ønsket om at modtage informationer fra brugeren. Nogle lægger man dårligt nok mærke til, mens andre er grænseløst irriterende (blandt disse er afgjort dialogboxe, der ønsker at få bekræftet, at man ved hvad man gør, og om man nu også er helt sikker!).
Man kan naturligvis lave dialogboxe som frames, men Java har en speciel klasse: JDialog, der særligt retter sig mod dialogboxe, med de specielle egenskaber disse har.
Window Man finder JDialog i javax.swing. Den er indplaceret i klasse­hierarkiet, som en subklasse til AWT's Dialog, der igen er en subklasse til Window, som er den nærmeste fælles superklasse med JFrame:
Figur 1:
Window's subklasser
Inden vi skal se nærmere på JDialog, vil vi først se på en teknikalitet:
1. Centrering af vinduer
Hvordan man centrerer vinduer på skærmen, kunne måske have været forklaret så mange andre steder i DocJava, end netop her. Jeg har dog fundet det naturligt at placere forklaringen her, da man næsten altid ønsker at en dialogbox skal placeres midt på skærmen - for at tiltrække opmærksomhed.
Selve centreringen består af fire trin:
  1. Find skærmens størrelse
  2. Find vinduets størrelse
  3. Beregn vinduets nye placering
  4. Placer vinduet på dets nye position
Vi vil først se hvordan vi rent teknisk kan udføre disse trin, og dernæst lave et design, som på fornuftig vis kan tilbyde denne funktionalitet.
1.1 Teknikken
Trin 1:
Man finder skærmens størrelse med:
Source 1:
Skærmens størrelse (i.e. opløsning)
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
getScreenSize-metoden returnerer en instans af Dimension, der beskriver størrelsen af skærmen. Man kan bruge et Toolkit til andre interessante ting, men vi vil her indskrænke os til ovenstående anvendelse.
Trin 2:
Som for ethvert andet grafisk komponent (subklasse til Component) kan vi finde et vindues størrelse med:
Source 2:
Vinduets størrelse
Dimension windowSize = vinduet.getSize();
  hvor vi antager at vinduet er en refrence af klassen Window.
  Igen returnerer metoden en Dimension, og vi er nu klar til at regne på tingene.
Trin 3:
Beregningen tager udgangspunkt i følgende betragtning:
Figur 2:
Centrering af rektangel i rektangel
  Vi kender både skærmen og vinduets bredde, og skal nu finde xpos. Man ser umiddelbart at xpos er det halve af den overskydende bredde:
xpos = ( Bredde af skærm - Bredde af vindue ) / 2
  Vi finder derfor xpos og ypos som:
Source 3:
Beregne placering af vindue
int xpos = (int) ( screenSize.getWidth()  - windowSize.getWidth()  ) / 2;
int ypos = (int) ( screenSize.getHeight() - windowSize.getHeight() ) / 2;
integer vs. double At vi må caste til int skyldes en designfejl i klassen Dimension. Dens datakerne består af to integers, men dens get-metoder returnerer doubles! En anden fejl ved Dimension, er at dens to instans­variable: width og height er public, men det spiller ingen rolle her.
Trin 4:
Vi er nu klar til justere vinduets placering, hvilket gøres med:
Source 4:
Placere vindue
vinduet.setLocation( normalize( xpos ), normalize( ypos ) );
Man bemærker at vi først normaliserer xpos og ypos. Det skyldes at vinduet godt kan være større end skærmen, og i så fald kan blive placeret med titelbaren udenfor skærmen, hvilket er uhensigtmæssigt. Vi ændrer derfor negative værdier til 0 med følgende service-metode:
Source 5:
Normalisere position
private int normalize( int pos ) {
  return ( pos < 0 ) ? 0 : pos;
}
1.2 Designet
Hvor skal vi placere denne funktionalitet? At placere service-metoder i samlige subklasser af JFrame og JDialog vil give en voldsom kode-redundans!
Mixin-klasse Man kan i al væsentlighed fjerne denne kode-redundans ved at lave mixin-klasser med centrerings-funktionaliteten. For JDialog kan vi lave følgende mixin-klasse:
Source 6:
Mixin-klasse til JDialog
JDialogMixin.java
public class JDialogMixin extends JDialog {
  
  public JDialogMixin( JFrame frame, String title, boolean modal ) {
    super( frame, title, modal );
  }
  
  protected void centerLocation() {
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension windowSize = getSize();
    
    int xpos = (int) ( screenSize.getWidth()  - windowSize.getWidth()  ) / 2;
    int ypos = (int) ( screenSize.getHeight() - windowSize.getHeight() ) / 2;
    
    setLocation( normalize( xpos ), normalize( ypos ) );
  }
  
  private int normalize( int pos ) {
    return ( pos < 0 ) ? 0 : pos;
  }
}
At der stadig er koderedundans tilbage, skyldes behovet for en fuldstændig tilsvarende mixin-klasse for JFrame:
Source 7:
Mixin-klasse til JFrame
JFrameMixin.java
public class JFrameMixin extends JFrame {
  
  public JFrameMixin( String title ) {
    super( title );
  }
  
  protected void centerLocation() {
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension windowSize = getSize();
    
    int xpos = (int) ( screenSize.getWidth()  - windowSize.getWidth()  ) / 2;
    int ypos = (int) ( screenSize.getHeight() - windowSize.getHeight() ) / 2;
    
    setLocation( normalize( xpos ), normalize( ypos ) );
  }
  
  private int normalize( int pos ) {
    return ( pos < 0 ) ? 0 : pos;
  }
}
Bemærk at centerLocation-metoden skal kaldes fra en subklasse efter dens størrelse er fastlagt - dvs. typisk efter pack eller setSize og før setVisible. Det skyldes naturligvis at getSize-kaldet i centerLocation-metoden ellers ville være misvisende.
En skabelon for anvendelsen kunne f.eks. være:
Source 8:
Subklasse der anvender center­Location
SomeDialog.java
public class SomeDialog extends JDialogMixin {
  
  public SomeDialog( JFrame frame, String title, boolean modal ) {
    super( frame, title, modal );
  
    ...
  
    pack();
    centerLocation();
    setVisible( true );
  }
}
Mange konstruk­torer En ulempe ved mixin-klasserne er konstruktorerne. Specielt JDialog har mange konstruktorer, og ønsker man at kunne anvende dem alle, skal de delegeres op igennem mixin-klassen - et ikke særlig spændende arbejde at skulle implementere dem. Vi har ovenfor kun implementeret de to konstruktorer som vil blive anvendt i dette kapitel.
Multipel ned­arvning Multipel nedarvning ville være kærkommen, da man derved kunne placere centrerings-funktionaliteten i en klasse for sig, og lade samtlige frames og dialogboxe nedarve fra den (ud over, at de naturligvis også skulle nedarve fra JFrame/JDialog).
Delegering Nogen kunne måske få den idé at løse problemet vha. komposition, nu hvor nedarvning viser sig kode-redundant. Man kan naturligvis gør det, men eftersom centrerings-funktionaliteten normalt kun ønskes udført én gang i løbet af et vindues levetid, vil et objekt, som man kan delegere denne opgave til, være overflødigt efter man har sendt én request til det. Komposition vil derfor være overkill.
Statisk Et sidste alternativ kunne være at gøre funktionaliteten statisk i en service-klasse. Det er naturligvis en mulighed, men statiske metoder er generelt et onde, og løsningen med en mixin-klasse synes derfor sundere i det lange løb.
2. class JDialog
Som nævnt har en JDialog nogle egenskaber, der retter sig specielt mod dialogboxe. Betragt følgende eksempel på en dialogbox (som vi vil implementere nedenfor):
Figur 3:
About dialogbox
Ikke minimeres maximeres Man bemærker at en JDialog ikke har de to små ikoner til minimize og maximize, som man ellers finder i øverste højre hjørne af JFrame's. Det betyder ikke, at man ikke kan resize en JDialog, men det kan kun ske ved at trække i kanterne af vinduet (Det vil normalt være uhensigtsmæssigt at tillade en dialogbox at blive resized, så man vil ofte anvende setResizable( false ) ).
Modal En anden særlig egenskab ved en dialogbox, er at den kan være modal. At en dialogbox er modal, vil sige at den ikke vil overlade fokus til andre vinduer - man skal afslutte dialogen før man kan gå videre. En modal dialogbox kan være irriterende, så man skal altid overveje om det er nødvendigt, når man designer sin brugergrænseflade. Som vi senere skal se, kan det være nemmere at implementere en modal dialogbox når det drejer sig om håndtering af input, men det bør naturligvis aldrig påvirke designvalget.
Det skal bemærkes, at den modale egenskab pt. er mangelfuld. I Windows (jeg har ikke prøvet det på andre platforme) vil en modal dialogbox kun være modal i relation til de øvrige vinduer i samme applikation. Det vil derfor være muligt at skifte til andre programmer i Windows.
Owner Enhver dialogbox har en såkaldt owner. En owner er den JFrame eller JDialog som dialogboxen tilhører. Hvis vi f.eks. åbner en dialogbox fordi brugeren har valgt et menupunkt i en JFrame, vil vi sætte den pågældende JFrame som owner af dialogboxen. En owner kan også være en anden dialogbox, hvis den er åbnet pga. interaktion med denne.
Lad os se hvordan man kan lave ovenstående about-dialogbox.
Først laver vi en frame, vi kan starte dialogboxen fra, og dermed få en owner til den:
Source 9:
Frame der laver dialogbox
StarterFrame.java
public class StarterFrame extends JFrameMixin implements ActionListener {
  private JButton aboutButton;
  
  public StarterFrame( String title ) {
    super( title );
    
    getContentPane().setLayout( new FlowLayout() );
    
    aboutButton = new JButton( "Start About-dialog" );
    aboutButton.addActionListener( this );
    getContentPane().add( aboutButton );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    pack();
    centerLocation();
    setVisible( true );
  }
  
  public void actionPerformed( ActionEvent e ) {
    Object source = e.getSource();
    
    if ( source == aboutButton )
      new AboutDialog( this );
  }
}
Framen får følgende simple udseende:
Figur 4:
Frame til at åbne dialogbox
Som det ses i kildeteksten, instantierer vi blot dialogboxen - vi gemmer ikke nogen reference til den.
Selve dialogboxen implementeres ved:
Source 10:
About-dialogbox
AboutDialog.java
public class AboutDialog extends JDialogMixin implements ActionListener {
  private JButton okayButton;
  
  public AboutDialog( JFrame owner ) {
    super( owner, "About", true );
    
    getContentPane().setLayout( new BorderLayout() );
      
      JLabel title = new JLabel( "WaterfallBuilder 0.9" );
      title.setFont( new Font( "arial black", Font.PLAIN, 24 ) );
      title.setBorder( new EmptyBorder( 5, 5, 5, 5 ) );
      title.setHorizontalAlignment( JLabel.CENTER );
    getContentPane().add( title, BorderLayout.NORTH );
    
      JLabel icon = new JLabel( new ImageIcon( "waterfall.jpg" ) );
      icon.setBorder( new EmptyBorder( 5, 5, 5, 5 ) );
    getContentPane().add( icon, BorderLayout.CENTER );
    
      JPanel sydPanel = new JPanel();
      sydPanel.setLayout( new BorderLayout() );
      sydPanel.setBorder( new EmptyBorder( 5, 5, 5, 5 ) );
    
      okayButton = new JButton( "Okay" );
      okayButton.addActionListener( this );
      okayButton.setHorizontalAlignment( JButton.RIGHT );
      sydPanel.add( okayButton, BorderLayout.EAST );
    getContentPane().add( sydPanel, BorderLayout.SOUTH );
    
    addWindowListener( new DialogCloser() );
    
    pack();
    setResizable( false );
    centerLocation();
    setVisible( true );
  }
  
  public void actionPerformed( ActionEvent e ) {
    closeDialog();
  }
  
  private void closeDialog() {
    dispose();
  }
  
  private class DialogCloser extends WindowAdapter {
    public void windowClosing() {
      closeDialog();
    }
  }
}
I kaldet til super-klassens konstruktor (et kald som konstruktoren i JDialogMixin delegerer videre til konstruktoren i JDialog) er der angivet tre parametre.
Mange konstruktorer Den første angiver en "ejer" af dialogboxen. Denne kan som nævnt være enten en JFrame eller en JDialog. JDialog har konstruktorer til dem begge. Samtidig har JDialog forskellige konstruktorer alt efter hvor mange af de tre parametre man ønsker at angive. Da vi har lavet en mixin-klasse, må vi i dénne implementere delegerende konstruktorer i den udstrækning vi ønsker at anvende JDialog's konstruktorer.
Titel Den næste parametere angiver dialogboxens titel som vi kender det for frames.
Modal Den tredie angiver boolsk om dialogboxen skal være modal. Vi har valgt at gøre vores dialogbox modal, da man normalt vil lade en About-dialog være modal. Det vil være uhensigtmæssigt at have en eller flere åbne About-dialoger åbne, og da brugeren uden videre kan lukke dem, vil den modale egenskab ikke være til gene.
Layout Vi vil ikke beskæftige os med layoutet, da det ikke adskiller sig fra frames. Svarende til frames anvendes en getContentPane-metode, og grafiske komponenter add'es til den returnerde Container. Default-layoutet er BorderLayout — igen det samme som for en frame.
3. Håndtering af input
En af de mere interessante designmæssige aspekter af dialogboxe, er hvordan man håndterer data indtastet af brugeren.
Vi vil i det følgende anvende en dialogbox til indtastning af brugernavn og password, som gennemgående eksempel. Dialogboxen har følgende udseende:
Figur 5:
Login dialogbox
Dialogboxen laves med følgende subklasse til JDialog(Mixin):
Source 11:
Login-dialogbox
LoginDialog.java
public class LoginDialog extends JDialogMixin implements ActionListener {
  private static int fontSize = 14;
  private static String fontName = "courier";
  private static int fontStyle = Font.BOLD;
  private static int textFieldWidth = 16;
  
  private JTextField userid, password;
  
  public LoginDialog( JFrame owner ) {
    super( owner, "Login", true );
    
    getContentPane().setLayout( new BorderLayout() );
    
    JLabel keyIcon = new JLabel( new ImageIcon( "key_icon.gif" ) );
    keyIcon.setBorder( new EmptyBorder( 10, 10, 10, 10 ) );
    
    getContentPane().add( keyIcon, BorderLayout.WEST );
    
    JPanel tekstPanel = new JPanel();
    tekstPanel.setLayout( new GridLayout( 0, 1 ) );
    tekstPanel.setBackground( Color.black );
    
    userid = new JTextField( textFieldWidth );
    userid.addActionListener( this );
    tekstPanel.add( wrap( userid, "Userid" ) );
    
    password = new JPasswordField( textFieldWidth );
    password.addActionListener( this );
    tekstPanel.add( wrap( password, "Password" ) );
    
    getContentPane().add( tekstPanel, BorderLayout.CENTER );
    
    pack();
    centerLocation();
    setVisible( true );
  }
  
  public void actionPerformed( ActionEvent e ) {
    System.out.println( "Hvad skal jeg gøre med de indtastede oplysninger?" );
    
    // håndtering af de indtastede oplysninger
    
    dispose();
  }
  
  private JPanel wrap( JTextField textField, String title ) {
    textField.setFont( new Font( fontName, fontStyle, fontSize ) );
    
    JPanel panel = new JPanel();
    panel.add( textField );
    panel.setBorder( new TitledBorder( new EtchedBorder( EtchedBorder.RAISED ), title ) );
    
    return panel;
  }
}
Hvad skal jeg gøre med de indtastede oplysninger?
Som man ser, vil ovenstående spørgsmål blive udskrevet når der trykkes return i et af tekstfelterne. Vores opgave, i resten af dette afsnit, er at finde et eller flere designmæssige svar på dette spørgsmål.
3.1 Modal blokering
Blokkerer En mulighed er at udnytte en speciel egenskab ved modale dialogboxe i Java — de blokkerer ved instantiering! Når vi i StarterFrame (se Source 9) laver en instans af LoginDialog kommer actionPerformed ikke længere, før instansen af LoginDialog kalder dispose-metoden i sin actionPerformed. Det betyder at vi umiddelbart efter instantieringen i StarterFrame kan bruge get-metoder til at aflæse de indtastede oplysninger.
I LoginDialog skal vi derfor have følgende metoder:
Source 12:
(LoginDialog).java
public String getUserid() {
  return userid.getText();
}

public String getPassword() {
  return password.getText();
}

public void actionPerformed( ActionEvent e ) {
  dispose();
}
Og actionPerformed-metoden i StarterFrame skal ændres til f.eks.:
Source 13:
(StarterFrame).java
public void actionPerformed( ActionEvent e ) {
  Object source = e.getSource();
  
  if ( source == aboutButton ) {
    LoginDialog dialog = new LoginDialog( this );
    
    System.out.println( "Der blev indtastet: " + dialog.getUserid() +
                        "/" + dialog.getPassword() );
  }
}
Der blev indtastet: johndoe/janedoe
Der kunne give ovenstående udskrift hvis vi indtastede brugernavnet: johndoe og password'et: janedoe.
Ved at anvende den blokerende egenskab ved modale dialogboxe, virker dialogboxens konstruktor nærmest som en get-metode, der returnerer et objekt med de indtastede data.
Hvis dialogboxens oplysninger skal kunne bruges før den lukkes, må vi dog anvende et andet design, da vi med denne løsning ikke kan håndtere de indtastede oplysninger før dialogboxen er termineret.
3.2 Oberver Pattern
update-metode Man kan i stedet vende det om, og bruge en eller flere set-metoder kaldt fra dialogboxen på framen eller andre objekter, der er interesseret i de indtastede oplysninger. Specielt i relation til det sidste, vil det være nærliggende at lade set-metoden være en update-metode i Observer Pattern.
Én observer I 99.9% af alle tilfælde vil der kun være én observer på dialogboxen, og vi vil derfor indskrænke os til at se vores login-eksempel med et objekt (framen) som eneste observer. For en mere generel løsning, med flere observere, henvises til kapitlet om Observer Pattern (Bemærk, at en løsning med flere observere normalt vil kræve at dialogboxen ikke er modal).
For at LoginDialog kan anvendes af forskellige klasser (dvs. ikke kun af StarterFrame) vil vi lave et generelt observer-interface til formålet:
Source 14:
LoginDialogObserver.java
public interface LoginDialogObserver {
  public void login( String userid, String password );
}
Som man ser, vælger vi at kalde update-metoden: login.
Dernæst lader vi StarterFrame implementere dette interface:
Source 15:
StarterFrame.java
public class StarterFrame extends JFrameMixin implements ActionListener, LoginDialogObserver {
  ...
  
  public void login( String userid, String password ) {
    System.out.println( "Der blev indtastet: " + userid + "/" + password );
  }
  
  public void actionPerformed( ActionEvent e ) {
    if ( e.getSource() == aboutButton )
      new LoginDialog( this, this );
  }
  
  ...
}
Bemærk, at det er nødvendigt at vi tilmelder StarterFrame som observer ved at angive den som parameter til konstruktoren, da en modal dialogbox ikke levner os mulighed for at sende den metodekald, idet den som bekendt blokkerer. Det er med andre ord ikke muligt at bruge en setObserver-metode på dialogboxen, da vi først vil kunne kalde den efter dialogboxen er lukket.
Man kunne umiddelbart få den idé, at det ikke var nødvendigt at angive this to gange som parameter til LoginDialog's konstruktor. Det er nødvendigt, da det ikke er sikkert at owner af dialogboxen og observer skal være det samme objekt; hvilket det dog er i vores eksempel.
Endelig har vi den modificerede LoginDialog; hvor actionPerformed kalder login-metoden i LoginDialogObserver'en:
Source 16:
LoginDialog.java
public class LoginDialog extends JDialogMixin implements ActionListener {
  ...
  
  private LoginDialogObserver observer;
  
  public LoginDialog( JFrame owner, LoginDialogObserver observer ) {
    super( owner, "Login", true );
    
    this.observer = observer;
    
    ...
  }
  
  public void actionPerformed( ActionEvent e ) {
    if ( observer != null )
      observer.login( userid.getText(), password.getText() );
  
    dispose();
  }
  
  ...
}
Call back Hvis man ser på løsningen med en modal dialogbox som et metode-kald, ligner Observer-løsningen det man normalt kalder call back - dvs. at man kalder en metode, der returnerer, for senere at foretage et kald tilbage til det "sted" den blev kaldt. Det er stadig instantieringen af dialogbox'en der er "kaldet", mens det er kaldet af login-metoden, der er selve call back.
3.3 Indre klasse
Fri adgang Det kunne være nærliggende at lade dialogboxen være en indre klasse i framen. På den måde vil den få fri adgang til framens metoder og datakerne, og f.eks. kunne tilgå objekter som framen har referencer til.
Man skal dog være opmærksom på at indre klasser kan gøre kildeteksten uoverskuelig, og da en dialogbox normalt ikke er nogen lille klasse, vil det sjældent være en god idé.
En implementation med en indre klasse vil dog i sig selv være ganske ukompliceret, og vi vil derfor undlade at konstruere et eksempel her.