Tämän yksinkertainen
-hakemiston dokumentteja ei ole päivitetty vuoden 2017 jälkeen, joten voi hyvin sisältää toimimattomia linkkejä ja tietoa joka on jo muuttunut. Kannattaa vakavasti miettiä katsooko näitä sivuja lainkaan, sillä käytännön erot esimerkkeihin, joissa on myös harrastukset mukana, ovat pieniä ja tämä yksinkertaistus voi vaan turhaan sotkea lukijaa, joka on kuitenkin tekemässä monipuolisempaa versiota.
Lisäksi monet linkit ovat vanhaan trac
-järjestelmään tai svn
-versionhallintaan ja Digipalvelut voivat lopettaa nuo palvelut milloin tahansa varoittamatta.
Eli jatka omalla vastuullasi :-)
Ohjelmointi 2 / yksinkertaistettu esimerkki tiedonsyötöstä
Tässä vuoden 2017 versio mallissa listoja käsitelään ListChooserilla
yms. Vuoden 2016 versiossa listoja käsitellään enemmän JavaFX:än tavoilla.
Tässä mallissa itse käyttöliittymää on yksinkertaistettu. Harjoitustyössä ei ole tarkoitus "peruuttaa" niin että harrastukset puuttuu, mutta tässä "peruutus" on tehty, jotta koodissa olisi vain oleellinen näkyvillä.
Aluksi toteuteus on "tyhmä", eli vaatii paljon get
ja set
-metodeja Jasen-luokkaan (joita on tehty laiskuuksissaan vaan 4 paria). Seuraavaksi (7.2) koodia on muutettu niin, että jäsenen kenttiä voidaan käydä läpi indeksoidusti.
- Koodit ja dokumentit vaihe 7.1.1 - jäsen näytetään jäsendialogissa (4 kenttää)
- Koodit ja dokumentit vaihe 7.1.2 - jäsen näytetään pääikkunassa (4 kenttää)
- Koodit ja dokumentit vaihe 7.1.3 - tietoa voidaan syöttää ja osin oikeellisuustarkistuskin (4 kenttää)
- Koodit ja dokumentit vaihe 7.2 - tietoa voidaan syöttää kaikkiin kenttiin, kentät indeksoidustsi
Seuraavissa koodeissa rivinumerot viittaavat ko. vaiheen versioon. "Lopullisessa" versioissa siten voi olla eri rivinumerot.
Käyttöliittymä
Alkutilanne
Vaihe 6 on sellainen, missä on luokat Kerho, Jasenet ja Jasen valmiina.
Yksinkertaistetussa käyttöliittymässä KerhoGUIController on tehty SceneBuilderillä
ja siinä toimii uuden "Aku Ankka"-jäsenen lisäys. Jäsenet näytetään ListChooser
-komponenttiin ja valitun jäsenen tiedot tulostetaan yhteen TextArea
-komponenttiin.
7.1.1 Tietojen näyttäminen Jäsen dialogissa
Seuraavassa vaiheessa on lisätty tietojen näyttäminen muokkausdialogiin niin, että jäsenen tiedot (tai neljä ensimmäistä kenttää) näkyvät kun muokataan jäsentä.
Muokkaamista varten lisätään kontrolleri JasenDialogController.java
Jäsen-dialogille.
Mallissa tiedot luetaan heti pääohjelman käynnistyessä eikä käyttäjä voi valita tiedoston nimeä:
KerhoGUIController.java
122 public boolean avaa() { 123 String uusinimi = kerhonnimi; 124 if (uusinimi == null) return false; 125 lueTiedosto(uusinimi); 126 return true; 127 }
Itse kerho
viedään lomakkeelle parametrina ja näin jos olisi useita lomakkeita, ne voisivat käyttää samaa kerhoa.
Kun jäsentä muokataan, käynnistetään uusi dialogi. Tosin toistaiseksi muokkausta ei tehdä, vaan pelkästään näytetään jäsenen tiedot.
KerhoGUIController.java
211 private void muokkaa() { 212 JasenDialogController.kysyJasen(null, jasenKohdalla); 213 }
Tehdään koodi niin, että aluksi käsitellään vaan neljää TextField
-kenttää, koska muuten tulisi hirveästi toistoa. JasenDialoView.fxml
:ssä annetaan nimet kentille:
Jatkossa tutkitaan miten toisto voidaan välttää. JasenDialogControllerissa
on annettu vastaavat nimet TextField
muuttujille:
JasenDialogController.java
024 @FXML private TextField editNimi; 025 @FXML private TextField editHetu; 026 @FXML private TextField editKatuosoite; 027 @FXML private TextField editPostinumero; 028 @FXML private Label labelVirhe; 029 030 @Override 031 public void initialize(URL url, ResourceBundle bundle) { 032 alusta(); 033 }
Nimet on liitetty SceneBuilderissä vastaaviin komponetteihin.
JasenDialogController.View
069 <TextField fx:id="editNimi" text="Ankka Aku" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" /> 070 <TextField fx:id="editHetu" text="010245-123U" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" /> 071 <TextField fx:id="editKatuosoite" text="Paratiisitie 13" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2" /> 072 <TextField fx:id="editPostinumero" text="12345" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3" />
Staattinen funktio kysyJasen
luo ja käynnistää dialogin modaalisena:
JasenDialogController.java
109 public static Jasen kysyJasen(Stage modalityStage, Jasen oletus) { 110 return ModalController.<Jasen, JasenDialogController>showModal( 111 JasenDialogController.class.getResource("JasenDialogView.fxml"), 112 "Kerho", 113 modalityStage, oletus, null 114 ); 115 }
Parametrina tuotu Jasen
-viite (nimellä oletus
) otetaan talteen ja näytetään sen jälkeen kun dialogi on täysin alustunut:
JasenDialogController.java
068 public void setDefault(Jasen oletus) { 069 jasenKohdalla = oletus; 070 naytaJasen(jasenKohdalla); 071 }
Itse jäsen näytetään laittamalla jäsenen tietoja vastaaviin TextField
-kenttiin:
JasenDialogController.java
093 public void naytaJasen(Jasen jasen) { 094 if (jasen == null) return; 095 editNimi.setText(jasen.getNimi()); 096 editHetu.setText(jasen.getHetu()); 097 editKatuosoite.setText(jasen.getKatuosoite()); 098 editPostinumero.setText(jasen.getPostinumero()); 099 }
Jasen- luokkaan on täytynyt lisätä get
-metodeja, jotta sieltä saadaan tietoa TextField
-kenttiin.
7.1.2 Tietojen näyttäminen myös päädialogissa
Samat koodit voitaisiin kirjoittaa myös KerhoGUIController
-luokkaan, mutta tästä tulisi toistoa. Siksi onkin ehkä helpompi, että tehdään taulukko, jossa on TextField
-komponetteja ja muutetaan JasenDialogController
-luokan metodeja staattisiksi, joille viedään parametrina tämä taulukko.
Esitellään ensin tekstikentät (muutos myös .fxml
-tiedostoon, jossa kentät voi samalla laittaa ei-editoitaviksi):
KerhoGUIController.java
027 @FXML private ListChooser<Jasen> chooserJasenet; 028 @FXML private TextField editNimi; 029 @FXML private TextField editHetu; 030 @FXML private TextField editKatuosoite; 031 @FXML private TextField editPostinumero; 032 033 @Override 034 public void initialize(URL url, ResourceBundle bundle) { 035 alusta(); 036 }
Sitten alustuksessa täytetään TextField
-taulukko.
KerhoGUIController.java
074 private Jasen jasenKohdalla; 075 private TextField edits[]; 076 077 078 /** 079 * Tekee tarvittavat muut alustukset, nyt vaihdetaan GridPanen tilalle 080 * yksi iso tekstikenttä, johon voidaan tulostaa jäsenten tiedot. 081 * Alustetaan myös jäsenlistan kuuntelija 082 */ 083 protected void alusta() { 084 chooserJasenet.clear(); 085 chooserJasenet.addSelectionListener(e -> naytaJasen()); 086 087 edits = new TextField[]{editNimi, editHetu, editKatuosoite, editPostinumero}; 088 }
Kun jäsen pitää näyttää, pyydetään JasenDialogControlleria
tekemään se:
KerhoGUIController.java
157 protected void naytaJasen() { 158 jasenKohdalla = chooserJasenet.getSelectedObject(); 159 if (jasenKohdalla == null) return; 160 161 JasenDialogController.naytaJasen(edits, jasenKohdalla); 162 }
Tehdään vastaavat muutokset JasenDialogControlleriin
:
JasenDialogController.java
046 private TextField edits[]; 047 048 049 /** 050 * Tyhjentään tekstikentät 051 * @param edits tauluko jossa tyhjennettäviä tektsikenttiä 052 */ 053 public static void tyhjenna(TextField[] edits) { 054 for (TextField edit : edits) 055 edit.setText(""); 056 }
062 protected void alusta() { 063 edits = new TextField[]{editNimi, editHetu, editKatuosoite, editPostinumero}; 064 }
068 public void setDefault(Jasen oletus) { 069 jasenKohdalla = oletus; 070 naytaJasen(edits, jasenKohdalla); 071 }
094 public static void naytaJasen(TextField[] edits, Jasen jasen) { 095 if (jasen == null) return; 096 edits[0].setText(jasen.getNimi()); 097 edits[1].setText(jasen.getHetu()); 098 edits[2].setText(jasen.getKatuosoite()); 099 edits[3].setText(jasen.getPostinumero()); 100 }
7.1.3 Tietojen siirto Jasen-olioon
Jäsenen muokkaamista varten täytyy jäsenestä tehdä kopio, jotta siihen tehdyt muutokset eivät vaikuta oikeaan ennenkuin muutos hyväksytään.
Tätä varten Jasen-luokkaan pitää lisätä clone
-metodi.
Jasen.java
016 public class Jasen implements Cloneable {
301 public Jasen clone() throws CloneNotSupportedException { 302 Jasen uusi; 303 uusi = (Jasen) super.clone(); 304 return uusi; 305 }
Aluksi siis tehdään jäsenestä kopio ja se viedään muokattavaksi. Kun muokkauksesta tullaan takaisin, palautetaan viite tähän kopioon jolla sitten tarvittaessa (ellei painettu Cancel
) korvataan vastaava olemassa oleva kerhon jäsen. Yhtäaikaisessa käytössä voisi periaattessa käydä niinkin, että joku olisi poistanut muokattavan jäsenen muokkauksen aikana ja siksi varaudutaan myös siihen, että jäsen joudutaan lisäämään takaisin.
KerhoGUIController.java
207 private void muokkaa() { 208 if ( jasenKohdalla == null ) return; 209 try { 210 Jasen jasen; 211 jasen = JasenDialogController.kysyJasen(null, jasenKohdalla.clone()); 212 if ( jasen == null ) return; 213 kerho.korvaaTaiLisaa(jasen); 214 hae(jasen.getTunnusNro()); 215 } catch (CloneNotSupportedException e) { 216 // 217 } catch (SailoException e) { 218 Dialogs.showMessageDialog(e.getMessage()); 219 } 220 }
Luokassa JasenDialogController
palautetaan joko jäsen tai null
sen mukaan painettiinko Ok
vaiko Cancel
.
JasenDialogController.java
036 @FXML private void handleOK() { 037 if ( jasenKohdalla != null && jasenKohdalla.getNimi().trim().equals("") ) { 038 naytaVirhe("Nimi ei saa olla tyhjä"); 039 return; 040 } 041 ModalController.closeStage(labelVirhe); 042 }
045 @FXML private void handleCancel() { 046 jasenKohdalla = null; 047 ModalController.closeStage(labelVirhe); 048 }
088 public Jasen getResult() { 089 return jasenKohdalla; 090 }
Luokassa JasenDialogController
on muutettu alustamismetodia niin, että neljälle (ei ole jaksettu laittaa useammalle) TextField
-komponentille laitetaan kuuntelijat sille, että näppäin nostetaan ylös. Laitetaan niin, että jos kentässä k
päästetään painike ylös, kutsutaan kasitteleMuutosJaseneen
-metodia niin että sille viedään parametrina k
ja käsitellyn kentän viite, joka saadaan selville tapahtuman käsittelijästä.
JasenDialogController.java
070 protected void alusta() { 071 edits = new TextField[]{editNimi, editHetu, editKatuosoite, editPostinumero}; 072 int i = 0; 073 for (TextField edit : edits) { 074 final int k = ++i; 075 edit.setOnKeyReleased( e -> kasitteleMuutosJaseneen(k, (TextField)(e.getSource()))); 076 } 077 }
Muutoskäsittelijässä otetaan TextField
-kentän sisältö ja annetaan se kentän indeksistä riippuen jäsenelle asetettavaksi. Asettamismetodit palauttavat tiedon siitä, onnistuuko asettaminen (null
) vaiko ei (virhe tekstinä jonka voi näyttää).
JasenDialogController.java
117 private void kasitteleMuutosJaseneen(int k, TextField edit) { 118 if (jasenKohdalla == null) return; 119 String s = edit.getText(); 120 String virhe = null; 121 switch (k) { 122 case 1 : virhe = jasenKohdalla.setNimi(s); break; 123 case 2 : virhe = jasenKohdalla.setHetu(s); break; 124 case 3 : virhe = jasenKohdalla.setKatuosoite(s); break; 125 case 4 : virhe = jasenKohdalla.setPostinumero(s); break; 126 default: 127 } 128 if (virhe == null) { 129 Dialogs.setToolTipText(edit,""); 130 edit.getStyleClass().removeAll("virhe"); 131 naytaVirhe(virhe); 132 } else { 133 Dialogs.setToolTipText(edit,virhe); 134 edit.getStyleClass().add("virhe"); 135 naytaVirhe(virhe); 136 } 137 }
Tässä muuten syy, miksi mallissa käsitellään vain neljää kenttää. Kukaan ei jaksa tehdä tuota useammalle. Oikeassa versiossahan kentät on indeksoitu, jolloin homma helpottuu huomattavasti. Tässäkin jokaisen TextField
tekstimuutos on laitettu menemään samaan käsittelijään kasitteleMuutosJaseneen
, jolle viedään parametrina muuttuneen kentän indeksi sekä TextField
, jossa muutos tapahtui. Muuten olisi jouduttu kirjoittamaan neljä lähes identtistä käsittelijää.
Jasen-luokaan on lisätty set
-metodeja, jotka palauttava null
jos asettaminen onnistuu ja muuten virhetiedon merkkijonona.
Jos tuli virhe, niin kentän väri vaihdetaan ja laitetaan virhetieto ToolTip
piin. Jos ei, niin ilmoitetaan kerholle, että jäsenen tiedot ovat muuttuneet, jolloin seuraava tallennus suostuu tiedot tallentamaan.
JasenDialogController.java
102 private void naytaVirhe(String virhe) { 103 if ( virhe == null || virhe.isEmpty() ) { 104 labelVirhe.setText(""); 105 labelVirhe.getStyleClass().removeAll("virhe"); 106 return; 107 } 108 labelVirhe.setText(virhe); 109 labelVirhe.getStyleClass().add("virhe"); 110 }
Jos lisätään uusi jäsen, luodaan se aluksi ja annetaan muokattavaksi. Mikäli muokkaus hyväksytään, rekisteröidään jäsen ja lisätään kerhoon. Samalla myös haetaan listaan jäsenet uudelleen niin, että uusi jäsen tulee oletuksena valituksi.
KerhoGUIController.java
192 protected void uusiJasen() { 193 try { 194 Jasen uusi = new Jasen(); 195 uusi = JasenDialogController.kysyJasen(null, uusi); 196 if ( uusi == null ) return; 197 uusi.rekisteroi(); 198 kerho.lisaa(uusi); 199 hae(uusi.getTunnusNro()); 200 } catch (SailoException e) { 201 Dialogs.showMessageDialog("Ongelmia uuden luomisessa " + e.getMessage()); 202 return; 203 } 204 }
7.2 Kentät indeksoidusti
Mikäli halutaan helposti käsitellä kaikkia kenttiä, kannattaa kentät ajatella taulukkomaisena rakenteena, jonka voi mennä silmukalla läpi. Muutetaan aluksi Jasen
-luokkaa niin, että se on ohjelman ainoa osa, joka tietää mitä kenttiä on olemassa ja kuinka monta.
Algoritmi:
1. Näyttö: montako kenttää sinulla on jäsen?
2. Jäsen: 13 kenttää
3. Näyttö: Mikä on ensimmäinen järkevä kentän indeksi kysyttäväksi
4. Jäsen: indeksi 1
5. Näyttö silmukassa 1..(13-1):
5.1 Näyttö: Anna kenttään i tarvittava otsikkoteksti
5.2 Jäsen: palauttaa merkkijonon (esim. nimi tai hetu)
5.3 Näyttö: luo lomakkeelle syöttökentän jonka vieressä ko merkkijono
6. Kun lomake käynnistetään tietylle jäsenelle
6.1 Tyhjennetään edit-kentät
6.2 Silmukassa i = 1..(13-1)
6.3 Näyttö: anna kentän i sisältö merkkijonona
6.4 Jäsen: palauttaa merkkijonon (esim. Ankka Aku tai 030451-111A)
6.5 Näyttö: laittaa merkkijonon syöttökentän oletusarvoksi
7. Näyttö: jää odottamaan kun käyttäjä täyttää lomaketta
7.1 Käyttäjä: muuttaa jotakin syöttöruutua indeksissä i
7.2 Näyttö: otetaan paikassa i oleva jono s
jasen, laita jono s kenttään i
7.3 Jasen: tarkistaa jonon s sopivuuden kenttään i.
Jos OK, sijoittaa sisällön kenttää i ja palauttaa OK.
Jos virhe, palauttaa virhetekstin
7.4 Näyttö: ottaa jäsenen palautuksen
Jos OK pyyhkii mahdolliset vanhat virheilmoitukset pois
Jos tulee virheilmoitus, näyttää sen käyttäjälle
Jasen.java
038 public int getKenttia() { 039 return 13; 040 } 041 042 043 /** 044 * Eka kenttä joka on mielekäs kysyttäväksi 045 * @return eknn kentän indeksi 046 */ 047 public int ekaKentta() { 048 return 1; 049 } 050 051 052 /** 053 * Alustetaan jäsenen merkkijono-attribuuti tyhjiksi jonoiksi 054 * ja tunnusnro = 0. 055 */ 056 public Jasen() { 057 // Toistaiseksi ei tarvita mitään 058 } 059 060 061 /** 062 * @return jäsenen nimi 063 * @example 064 * <pre name="test"> 065 * Jasen aku = new Jasen(); 066 * aku.vastaaAkuAnkka(); 067 * aku.getNimi() =R= "Ankka Aku .*"; 068 * </pre> 069 */ 070 public String getNimi() { 071 return nimi; 072 } 073 074 075 /** 076 * Antaa k:n kentän sisällön merkkijonona 077 * @param k monenenko kentän sisältö palautetaan 078 * @return kentän sisältö merkkijonona 079 */ 080 public String anna(int k) { 081 switch ( k ) { 082 case 0: return "" + tunnusNro; 083 case 1: return "" + nimi; 084 case 2: return "" + hetu;
...
116 public String aseta(int k, String jono) { 117 String tjono = jono.trim(); 118 StringBuffer sb = new StringBuffer(tjono); 119 switch ( k ) { 120 case 0: 121 setTunnusNro(Mjonot.erota(sb, '§', getTunnusNro())); 122 return null; 123 case 1: 124 nimi = tjono; 125 return null; 126 case 2:
...
177 public String getKysymys(int k) { 178 switch ( k ) { 179 case 0: return "Tunnus nro"; 180 case 1: return "nimi"; 181 case 2: return "hetu";
Sitten muutetaan käyttöliittymäluokkaa niin, että poistetaan varulta kaikki komponentit GridPane
-komponentista ja luodaan uudet tilalle kysellen jäseneltä kuinka monta kenttää on ja nimisiä ne ovat. Tätä varten on luoto yksi apujasen
-olio, joka on olemassa koko ohjelman aja, mutta jota käytetään vain kenttätiedon kyselemiseen.
JasenDialogController.java
054 private static Jasen apujasen = new Jasen(); // Jäsen jolta voidaan kysellä tietoja. 055 private TextField[] edits;
064 public static TextField[] luoKentat(GridPane gridJasen) { 065 gridJasen.getChildren().clear(); 066 TextField[] edits = new TextField[apujasen.getKenttia()]; 067 068 for (int i=0, k = apujasen.ekaKentta(); k < apujasen.getKenttia(); k++, i++) { 069 Label label = new Label(apujasen.getKysymys(k)); 070 gridJasen.add(label, 0, i); 071 TextField edit = new TextField(); 072 edits[k] = edit; 073 edit.setId("e"+k); 074 gridJasen.add(edit, 1, i); 075 } 076 return edits; 077 }
106 protected void alusta() { 107 edits = luoKentat(gridJasen); 108 for (TextField edit : edits) 109 if ( edit != null ) 110 edit.setOnKeyReleased( e -> kasitteleMuutosJaseneen((TextField)(e.getSource()))); 111 panelJasen.setFitToHeight(true); 112 }
Vastaavasti laitajasen
muutetaan niin, että se kyselee kohdalla olevalta jäseneltä tietoja:
JasenDialogController.java
181 public static void naytaJasen(TextField[] edits, Jasen jasen) { 182 if (jasen == null) return; 183 for (int k = jasen.ekaKentta(); k < jasen.getKenttia(); k++) { 184 edits[k].setText(jasen.anna(k)); 185 } 186 }
Lopuksi tietojen laittaminen jäseneen suoraviivaistuu:
JasenDialogController.java
095 public static int getFieldId(Object obj, int oletus) { 096 if ( !( obj instanceof Node)) return oletus; 097 Node node = (Node)obj; 098 return Mjonot.erotaInt(node.getId().substring(1),oletus); 099 }
158 protected void kasitteleMuutosJaseneen(TextField edit) { 159 if (jasenKohdalla == null) return; 160 int k = getFieldId(edit,apujasen.ekaKentta()); 161 String s = edit.getText(); 162 String virhe = null; 163 virhe = jasenKohdalla.aseta(k,s); 164 if (virhe == null) { 165 Dialogs.setToolTipText(edit,""); 166 edit.getStyleClass().removeAll("virhe"); 167 naytaVirhe(virhe); 168 } else { 169 Dialogs.setToolTipText(edit,virhe); 170 edit.getStyleClass().add("virhe"); 171 naytaVirhe(virhe); 172 } 173 }
Tehdään vastaavia muutoksia myös KerhoGuiControlleriin
jotta jäsentä voidaan muokata hiiren tuplaklikkauksella ja että TextField
-kentätä ovat ei-editoitavia.
KerhoGUIController.java
084 protected void alusta() { 085 chooserJasenet.clear(); 086 chooserJasenet.addSelectionListener(e -> naytaJasen()); 087 edits = JasenDialogController.luoKentat(gridJasen); 088 for (TextField edit: edits) 089 if ( edit != null ) { 090 edit.setEditable(false); 091 edit.setOnMouseClicked(e -> { if ( e.getClickCount() > 1 ) muokkaa(getFieldId(e.getSource(),0)); }); 092 edit.focusedProperty().addListener((a,o,n) -> kentta = getFieldId(edit,kentta)); 093 } 094 }
Attribuutin kentta
avulla voidaan siirtyä muokkauksessa siihen kenttään, joka oli aktiivinen kun muokkausta pyydettiin.
JasenDialogController.java
128 private void setKentta(int kentta) { 129 this.kentta = kentta; 130 }
...
137 public void handleShown() { 138 kentta = Math.max(apujasen.ekaKentta(), Math.min(kentta, apujasen.getKenttia()-1)); 139 edits[kentta].requestFocus(); 140 }
...
197 public static Jasen kysyJasen(Stage modalityStage, Jasen oletus, int kentta) { 198 return ModalController.<Jasen, JasenDialogController>showModal( 199 JasenDialogController.class.getResource("JasenDialogView.fxml"), 200 "Kerho", 201 modalityStage, oletus, 202 ctrl -> ctrl.setKentta(kentta) 203 ); 204 }
Nyt ohjelma on saatu sellaiseen muotoon, jossa voidaan muuttaa kenttätietoa Jasen
-luokassa ja muuta ei tarvitsekkaan tehdä, jotta myös käyttöliittymä muuttuu. Toki Jasen
-luokka on vielä toteutettu hölmösti switch
-lauseilla ja seuraava muutos olisikin korvata nuokin taulukolla.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.