6. gyakorlat¶
Contact alkalmazás folytatása¶
Az előző óra végén a listából a kontaktok törlését megvalósítottuk.
Mielőtt a módosítást megcsinálnánk elkészítjük a fő ablak menüjében a Close
menüpont eseménykezelését.
1 2 3 4 5 6 7 8 |
|
Az FXML-ben a következőnek kell szerepelnie, melyet a SceneBuilder-rel is megadhatunk (Code -> onAction
):
1 |
|
Ezzel meg is vagyunk, így nézzük az editContact
metódust!
Mivel a szerkesztéshez egy új ablakra lesz szükségünk, ahol magát a kontaktot szerkeszteni tudjuk, így létre is fogunk hozni egyet /fxml/add_edit_contact.fxml
néven a resources
mappa alatt és ezt az FXML állományt fogjuk betölteni.
Ezt a formot majd a hozzáadásnál is használhatjuk, így ilyen szemlélettel alakítjuk ki a felületet, illetve a neve is ezt tükrözi.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Mivel a form-on szeretnénk majd megjeleníteni a szerkeszteni kívánt kontakt információit, így ezt át fogjuk adni a controller számára. Hozzuk is létre az FXML állományt és a hozzá tartozó AddEditContactController-t! Készítsük el a felületet mely a következőképpen néz ki a SceneBuilder-ben: A bal oldalon látható TreeView segítséget nyújt a pontos elrendezés kialakításában
Az elemek pontos tulajdonságánál segítséget nyújt az FXML kód, melyből láthatjuk a megadott fx:id
-kat és az eseménykezelőket is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
|
Ezen a ponton ellenőrizzük, hogy az edit contact gomb megnyomása esetén sikerül-e betölteni az FXML állományt, de ehhez először szükségünk van a setContact
metódus definíciójára.
1 2 3 4 5 6 7 8 |
|
Fejlesszük tovább az AddEditContactController
-t úgy, hogy az FXML-ben megadott vezérlőket injektáljuk a controller-be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Ezután a felületi vezérlők értékét (a fent injektált elemek) be szeretném állítani a megkapott contact megfelelő property értékeire.
Így itt kötéseket fogok alkalmazni, melyet akkor hozok létre, amikor a setContact
-ot meghívja valaki (azaz új kontakt kerül a felülethez).
A setContact
ennek megfelelően a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Mivel a contact alapvetően nem tud a hozzá tartozó phone-okról, így ezt a phoneDAO-tól kérjük le, melyet field-ként adjunk is meg a controller-ben:
1 |
|
A ListView
minket érdeklő property-je az itemsProperty
, mely a benne lévő elemeket adja meg ObjectProperty <ObservableList <T>>
formájában, ahol a T
típusparaméter jelen esetben nyilván Phone
.
A Contact
bean-ben nem véletlenül ilyen módon adjuk meg a telefonokat (ObjectProperty<ObservableList<Phone>>
).
A dateOfBirth
vezérlő egy valueProperty
-vel rendelkezik, mely visszaad egy ObjectProperty<T>
property-t, ahol T
jelenleg LocalDate
, melyet szintén ilyen módon adtunk meg a Contact
osztályban.
A kétirányú kötés inicializálása fontos, mégpedig olyan szempontból, hogy az értékeket szinkronba fogjuk hozni és így az egyik property elveszti a jelenlegi értékét.
Ha vesszük például a name
property kötését, akkor a name
felületi vezérlő értékét kötjük hozzá a contact
nameProperty
-jéhez, azaz a contact
értéke másolódik a name
felületi vezérlő property-jébe.
Fordított esetben, azaz: contact.nameProperty().bindBidirectional(name.textProperty());
esetében a contact
objektumunk elvesztette volna az aktuális értékét és a felületen sem jelenne meg semmilyen érték.
Ezen a ponton ellenőrizzük, hogy a megadott elemek megjelennek-e a felületen (Az adatbázisban adjunk meg ).
A telefonok listájában, azaz a ListView<Phone>
elemben az egy cellában megjelenő elemekre alapból a toString()
-et hívja a rendszer és ezt írja ki a cellába, ezért láthatunk rosszul megjelenő (Object osztály toString()
alapú) adatokat.
Nincs más teendő, mint a Phone
osztály toString
metódusát elkészíteni:
1 2 3 4 5 6 7 8 |
|
Ha ezekkel megvagyunk akkor jöhet a tényleges mentés, de előtte implementáljuk a Cancel gomb eseménykezelését, mely szimplán annyit tesz, hogy visszatölti a fő ablakot.
1 2 3 4 |
|
A mentéshez szükségünk lesz egy ContactDAO
-ra, így a következő field megadást is használjuk:
1 |
|
Maga a mentés a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 10 |
|
Elsőre furán nézhet ki a telefonszámok törlése, majd létrehozása. Ezt azért csináljuk ilyen módon, mert majd a telefonszámokat is módosítani szeretnénk, ugyanakkor azokat egyből nem szeretnénk menteni, csak akkor amikor a kontakt szerkesztés/hozzáadás felületen megnyomjuk a Save gombot. Mivel kicsit nehezebb így számontartani, hogy mi változott, a legegyszerűbb az, ha minden számot kitörlünk és újra hozzáadjuk őket (lehet, hogy nem is változott semmi, de ettől még működni fog a fenti kódrészlet csak kis plusz melót végez).
A deleteAll
phoneDAO
metódus még nem létezik, de azt nagyon könnyen implementálhatjuk (nyilván adjuk hozzá az interface-hez is a metódus fejlécét):
1 2 3 4 |
|
Miután elmentettük a contact
-ot és a telefonszámokat, ebben az esetben is visszatérünk a fő ablakhoz.
A telefonok listájánál végigvihetjük a kontakt táblázatban látott megvalósítást is, amikor az Edit
és a Delete
gombokat hozzáadtuk egy külön oszlopban.
A ListView
-nál ez annyiban különbözne, hogy valamilyen Label
-t is hozzáadnánk, melyen magát a telefonszámot adjuk meg, továbbá a két gombot is.
Ez azért kell mert a ListView
csak elemek sora, azaz olyan mintha egy egy oszlopos TableView
-nk lenne.
Annak érdekében, hogy egy újabb lehetőséget ismerjünk meg, így egy ContextMenu
fogunk készíteni (jobb klikk az adott ListView
elemen), melyben az Edit
és a Delete
menüpontokat fogjuk elhelyezni.
Ehhez szükségünk lesz az initialize
metódusra, így a controllert implementálja az Initializable
interface-t!
Maga az initialize
a következőképpen alakul:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
A setCellFactory(Callback <ListView<T>,ListCell<T>> value)
-ban megadott CAllback
egy ListCell<T>
ad vissza.
Ezt a cellát létre is hozzuk és később fel is használjuk, előtte azonban létrehozzuk a ContextMenu
-t és hozzáadjuk a két MenuItem
objektumot, amelyeket szintén itt hoztunk létre.
A gombok viselkedését később nézzük meg, most elég a többi részt értelmeznünk, ahhoz, hogy a context menüt meg tudjuk jeleníteni.
Mivel egy ListView
tartalmazhat több sort, mint amennyi tényleges Phone
van benne így kezelnünk kell azt az esetet, ha az adott sor üres.
A cella szövegét ettől tesszük függővé a new When(cell.itemProperty().isNotNull()).then(cell.itemProperty().asString()).otherwise("");
kötés létrehozásával, mely kötés aktuális értékét hozzákötjük a cella textProperty-kéhez
.
Ezen felül csinálunk egy ChangeListener
-t is, mely azt figyeli, hogy ha az adott cella értéke üres lesz akkor nem ad hozzá context menüt (cell.setContextMenu(null);
), illetve ha van érték a sorban, akkor hozzáadja a context menüt.
Végül visszaadjuk magát a cell
-t.
Ezután nézzük az eseménykezelőket!
A legfontosabb, hogy tudnunk kell, hogy melyik soron kattintottunk a kontextus menü elemekre (melyik Phone
objektumot szeretnénk módosítani vagy törölni).
Ehhez a cell.getItem()
metódus ad segítséget, mely visszaadja a cellához rendelt Phone
objektumot.
Kezdjük a törléssel, mert az egyszerűbb:
1 2 3 |
|
Mivel a contact
mentésénél törlünk minden az adott contact-hoz rendelt Phone
-t, majd utána a következőképpen mentjük a telefonszámokat: contact.getPhones().forEach(...)
, így elegendő, ha a contact.getPhones()
listából kitöröljük azt a Phone
-t, amelynek cellájában a context menü törlést kiválasztottuk.
Fontos látni, hogy ezen a ponton az adatbázisban semmit sem matattunk.
A módosításhoz szükség lesz egy form-ra, melyet egy modális ablakban (nem lehet belőle kikattintani) szeretnénk megjeleníteni.
Ezért itt hívjuk meg a showPhoneDialog
metódust az adott Phone
típusú paraméterrel!
1 2 3 4 |
|
Maga a showPhoneDialog
a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
A kód nagy része megegyezik azzal amit az App.loadFXML
-en belül megcsináltunk.
Itt egy saját Stage
-be szeretném belerakni a tartalmat, így a fenti megközelítést alkalmazom.
Ami fontos, hogy a kontrolleren beállítjuk a stage
, phone
és contact
objektumokat, mivel ezekre szükség lesz (később látjuk).
Challenge
Oldjuk meg úgy, hogy az eddigi hívásokat ne kelljen átírnunk, viszont legyen lehetőség arra is, hogy a fenti problémát kiküszöböljük.
Az egyik probléma az, hogy van olyan eset, amikor műveleteket is szeretnénk elvégezni a controller-en, a másik egyszerűbb probléma, hogy mi legyen a Stage
-el.
Megoldás
Az App.java
a következőképpen változik:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Ilyen módon sehol nem kell változtatnunk a meglévő loadFXML
hívásokon, azonban a showPhoneDialog
-on belül tudjuk a következőt csinálni:
1 2 3 4 5 6 |
|
Kössük be az Add gombhoz is a showPhoneDialog
-ot (onAction
legyen beállítva a gombon):
1 2 3 4 5 6 7 8 9 10 |
|
Ezután hozzuk létre az /fxml/add_edit_phone.fxml
állományt, illetve a hozzá tartozó AddEditPhoneController
-t.
Hozzuk is létre a controller-ben az init metódust:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Miután ezzel megvagyunk, alakítsuk ki a következő felületet a SceneBuilder segítségével (itt is segít a bal oldali tree view):
Az FXML-hez tartozó, generált FXML kód a követkető:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Az egyetlen új vezérlő a ComboBox
, melyet a PhoneType
-nál alkalmazunk.
Ezen a ponton ellenőrizhetjük, hogy megfelelően betölti-e az oldalt az alkalmazás (mind az Add és mind az Edit esetén).
Ezután a controllerbe injektáljuk a szükséges grafikus vezérlő elemeket:
1 2 3 4 5 |
|
A ComboBox
választható elemeinek a megadásához használjuk az Initializable
interface initialize
metódusát:
1 |
|
Itt lekérjük az összes lehetséges enum értéket és ezt állítjuk be lenyíló doboznak.
A korábban megírt init
metódusban ezután a kapott Phone
tulajdonságok alapján hozzuk létre a felületi kötéseket, hasonlóan mint ahogy azt a Contact
esetében is megtettük:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A kötések létrehozása után megvizsgáljuk, hogy az adott Phone
-hoz tartozik-e phoneType
(kötés miatt ezt látjuk már), amennyiben nem akkor egy új telefonról van szó és ilyen esetben az első lehetséges opciót választjuk ki az enum értékei közül.
A felületen elhelyezett Cancel
gomb hatására az init-ben beállított stage-et fogom bezárni:
1 2 3 4 |
|
A Save
egy kicsit fura felépítésű lesz:
1 2 3 4 5 6 |
|
A törlés és utána hozzáadás ugyanarra az objektumra redundánsnak tűnik, de mivel ez felelős a hozzáadás és a módosításért is, így van létjogosultsága (új elem esetén nyilván nem tudja kitörölni, csak hozzáadni).
Ellenőrzések és kiegészítések¶
Az első egy apró kiegészítés a kontakt törléshez, mivel a kontakthoz rendelt telefonszámokat nem töröltük ki. A megerősítés után ezt megtehetjük a következőképpen:
1 2 3 4 5 6 7 8 9 |
|
Contact ellenőrzések¶
Ezután végezzünk el a kötelező elemek megadásának ellenőrzését az AddEditContactController
-ben:
Először is le szeretnénk tiltani a gombot (nem lehet megnyomni), addig amíg van érvénytelen adat.
Helyezzük el a két Label
-t a felületen, ahova a hibaüzeneteket fogjuk írni, illetve injektáljuk is ezeket a controllerbe:
1 2 3 4 5 6 7 8 |
|
Az initialize
metódust ezután a következőképpen módosítjuk:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Így, a gomb egészen addig disabled
állapotban lesz, ameddig vagy a name vagy az email vagy a születési dátum üres/null.
Az egyes hibajelző Label
-ek értékét ChangeListener
-ek alapján állítjuk be.
Keresés a kontaktok listájában¶
Először alakítsuk ki a felületi vezérlőket a main_window.fxml
oldalon.
A BorderPane
bal oldali eleméhez adjuk hozzá a következőket:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A MainWindowController
-en belül injektáljuk a két TextField
objektumot, majd készítsük el az onSearch
metódust.
Jelen esetben nem adatázisműveletként valósítjuk meg a keresést (bár valós környezetben így volna szép), hanem a lekért teljes listát filterezzük.
Emiatt a refreshTable
-t módosítjuk úgy, hogy a kontaktok listáját egy field-ben áltároljuk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Az onSearch
-ön belül Stream API-t használunk az elemek szűréséhez, majd ezt állítjuk be a táblázat elemeiként.
Videók¶
Gyakorló Projekt - Pizzázás¶
A feladat egy pizzanyilvántartó program elkészítése Java nyelven, amely követi az MVC modellt. A programnak a következő funkcionalitásokat kell támogatnia.
Pizza felvétele¶
A programnak támogatnia kell a pizza felvételét, ahol a következő adatokat kell felvennie:
- Pizza neve, ami szövegesen adható meg
- Pizza leírása, ami tartalmazza, hogy milyen összetevői vannak a pizzának
- Vegetáriánus jelző (Checkbox)
- És végül az ára, ami egy egész szám (Spinner, Slider)
Megszorítások, követelmények:
- A pizza beszúrásakor kapjon egy egyedi azonosítót.
- A sikeres beszúráskor egy ablak jelenjen meg, amely megmondja a beszúrt pizza sorszámát.
- A rendszer nem tárolhat két ugyanolyan nevű pizzát. Ha ugyanolyan nevű pizzát akarunk beszúrni, akkor a rendszer adjon hibaüzenetet (de ne zárja be a beviteli ablakot).
Pizzák listázása¶
A programnak támogatnia kell a pizzák listázását. Nem kell, hogy automatikusan történjen, elegendő valamilyen funkcionalitást biztosítani erre.
Pizza rendelés¶
A programnak támogatni kell a pizza rendelést is. Ez azt jelenti, hogy a felhasználó kiválaszthatja, hogy milyen pizzát szeretne rendelni, mekkora méretűt, hány darabot, és akkor megrendelheti.
- A pizzát kiválasztani a legördülő menü segítségével lehet, ahol az összes elérhető pizza közül lehet választani.
- A kiválasztott pizza tulajdonságai jelenjen meg alatta.
- A kicsi pizza 20%-kal olcsóbb, mint a normál méretű, míg a nagy 20%-kal drágább.
Típushibák¶
Fordítási hibák¶
- Nem implementált örökölt metódus
- Absztrakt osztály példányosítása
- FXML
- Rossz osztály van megadva
- @FXML annotáció hiánya
- Rossz az FXML fájl útvonala
- Rossz import:
javafx.valami
helyett példáuljava.awt.valami
- Örökölt metódus láthatósága csökken
Futási hibák¶
Adatbázis, DAO¶
- Rossz az SQL parancs, ami létrehozza a kapcsolatot az adatbázis felé
getConnection("jdbc:sqlite:" + DBFILE);
helyettgetConnection("jdbc:sqlite" + DBFILE);
getConnection("jdbc:sqlite: + DBFILE");
getConnection("jdbc.sqlite:" + DBFILE);
INSERT INTO Pizza (Nev, Leiras, Vega, Ar) VALUES (?, ?, ?, ?)
hibás- Hibás táblanév, mezővés
VALUES
helyettVALUE
PreparedStatement
esetében asetXXX
esetében az indexelésnek 1-től kell indulnia, és nem 0-tól- A DAO-ban Adatbázis fájl rosszul van megadva
View¶
- Rosszul összerakott GUI
- Egy elem kétszer is előfordul a fában
Logikai hibák¶
Olyan hibák, amelyek nem vezetnek futási hibához, de nem jól működik a program
- A DAO vagy Controller esetében az interfésznek kell lennie a statikus típusnak, és a konkrét megvalósítás a dinamikus típus
- DAO-ban a list esetében nem töröljük a listát, mielőtt ismét feltöltenénk
ConstructTable
többszöri meghívása (plusz oszlopok lesznek minden hívásnál)- Rossz képletek, inicializálatlan értékek, objektumok