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 klassehierarkiet, som en subklasse til AWT's Dialog, der igen er en subklasse til Window, som er den nærmeste fælles superklasse med JFrame: |
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: | |
|
|
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: | |
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: | |
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: | |
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: | |
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 instansvariable: 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: | |
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: | |
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: |
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: | |
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: | |
SomeDialog.java
public class SomeDialog extends JDialogMixin { public SomeDialog( JFrame frame, String title, boolean modal ) { super( frame, title, modal ); ... pack(); centerLocation(); setVisible( true ); } } |
|
Mange konstruktorer | 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 nedarvning | 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): | |
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: | |
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: | |
Som det ses i kildeteksten, instantierer vi blot dialogboxen - vi gemmer ikke nogen reference til den. | |
Selve dialogboxen implementeres ved: | |
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: | |
Dialogboxen laves med følgende subklasse til JDialog(Mixin): | |
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: | |
(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.: | (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: | |
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: | |
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: | |
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. |