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.

Seuraavissa koodeissa rivinumerot viittaavat ko. vaiheen versioon. "Lopullisessa" versioissa siten voi olla eri rivinumerot.

Käyttöliittymä

Käyttöliittymä
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.

Muutokset alkutilanteeseen

Mallissa tiedot luetaan heti pääohjelman käynnistyessä eikä käyttäjä voi valita tiedoston nimeä:

# c2

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.

# c4

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     }
# tiedonsyotto

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 ToolTippiin. 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     }
# indeksoidusti

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
# c3

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;
# c5

...

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:
# c6

...

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.