table of contents · eventbus 16. workspaces 17. internationalization 18. config settings and state...
TRANSCRIPT
-
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8
1.9
1.10
1.11
1.12
1.13
1.14
1.15
1.16
1.17
1.18
1.19
1.20
1.21
1.22
1.23
1.24
TableofContentsIntroduction
1.WhyTornadoFX?
2.SettingUp
3.Components
4.BasicControls
5.DataControls
6.TypeSafeCSS
7.LayoutsandMenus
8.Charts
9.ShapesandAnimation
10.FXML
11.EditingModelsandValidation
12.OSGi
13.TornadoFXIDEAPlugin
14.Scopes
15.EventBus
16.Workspaces
17.Internationalization
18.ConfigSettingsandState
19.JSONandREST
20.DependencyInjection
21.Wizard
AppendixA-SupplementaryTopics
AppendixB-ToolsandUtilities
1
-
TornadoFXGuideThisisacontinualefforttofullydocumenttheTornadoFXframeworkintheformatofabook.
Introduction
2
https://github.com/edvin/tornadofx
-
IntroductionUserinterfacesarebecomingincreasinglycriticaltothesuccessofconsumerandbusinessapplications.Withtheriseofconsumermobileappsandwebapplications,businessusersareincreasinglyholdingenterpriseapplicationstoahigherstandardofquality.Theywantrich,feature-packeduserinterfacesthatprovideimmediateinsightandnavigatecomplexscreensintuitively.Moreimportantly,theywanttheapplicationtoadaptquicklytobusinesschangesonafrequentbasis.Forthedeveloper,thismeanstheapplicationmustnotonlybemaintainablebutalsoevolvable.TornadoFXseekstoassistalltheseobjectivesandgreatlystreamlinethecodingofJavaFXUI's.
WhilemuchoftheenterpriseITworldispushingHTML5andcloud-basedapplications,manybusinessesarestillusingdesktopUIframeworkslikeJavaFX.Whileitdoesnotdistributetolargeaudiencesaseasilyaswebapplications,JavaFXworkswellfor"in-house"businessapplications.Itshigh-performancewithlargedatasets(andthefactitisnativeJava)makeitapracticalchoiceforapplicationsusedbehindthecorporatefirewall.
JavaFX,likemanyUIframeworks,canquicklybecomeverboseanddifficulttomaintain.Fortunately,thereleaseofKotlinhascreatedanopportunitytorethinkhowJavaFXapplicationsarebuilt.
AninterestingproductthatisindevelopmentisJPro,aweb-basedJavaFXcontainerthatusesnoplugins.ItcanworkwithTornadoFXandJavaFX,butisstillinclosedbetaatthetimeofwriting.Youcanfollowtheprojectandwaitforitsavailabilityhere:https://jpro.io/
WhyTornadoFX?InFebruary2016,JetBrainsreleasedKotlin,anewJVMlanguagethatemphasizespragmatismoverconvention.KotlinworksatahigherlevelofabstractionandprovidespracticallanguagefeaturesnotavailableinJava.OneofthemoreimportantfeaturesofKotlinisits100%interoperabilitywithexistingJavalibrariesandcodebases,includingJavaFX.
WhileJavaFXcanbeusedwithKotlininthesamemannerasJava,somebelievedKotlinhadlanguagefeaturesthatcouldstreamlineandsimplifyJavaFXdevelopment.WellbeforeKotlin'sbeta,EugenKissprototypedJavaFX"builders"withKotlinFX.InJanuary2016,EdvinSyserebootedtheinitiativeandreleasedTornadoFX.
1.WhyTornadoFX?
3
https://jpro.io/http://kotlinlang.org
-
TornadoFXseekstogreatlyminimizetheamountofcodeneededtobuildJavaFXapplications.Itnotonlyincludestype-safebuilderstoquicklylayoutcontrolsanduserinterfaces,butalsofeaturesdependencyinjection,delegatedproperties,controlextensionfunctions,andotherpracticalfeaturesenabledbyKotlin.TornadoFXisafineshowcaseofhowKotlincansimplifycodebases,andittacklestheverbosityofUIcodewitheleganceandsimplicity.ItcanworkinconjunctionwithotherpopularJavaFXlibrariessuchasControlsFXandJFXtras.ItworksespeciallywellwithreactiveframeworkssuchasReactFXaswellasRxJavaandfriends(includingRxJavaFX,RxKotlin,andRxKotlinFX).
ReaderRequirementsThisbookexpectsreaderstohavesomeknowledgeofKotlinandhavespentsometimegettingacquaintedwithit.TherewillbesomecoverageofKotlinlanguagefeaturesbutonlytoacertainextent.Ifyouhavenotdonesoalready,readtheJetBrainsKotlinReferenceandspendagoodfewhoursstudyingit.
ItdefinitelyhelpstobefamiliarwithJavaFXbutitisnotarequirement.PerhapsyoustartedstudyingJavaFXbutfoundthedevelopmentexperiencetobetedious,andyouarecheckingoutTornadoFXhopingitprovidesabetterwaytobuilduserinterfaces.IfthisdescribesyourexperienceandyouarelearningKotlin,thenyouwillprobablybenefitfromthisguide.
AMotivationalExampleIfyouhaveworkedwithJavaFXbefore,youmighthavecreatedaTableViewatsomepoint.SayyouhaveagivendomaintypePerson.TornadoFXallowsyoutomuchmoreconciselycreatetheJavaBeans-likeconventionusedfortheJavaFXbinding.
classPerson(id:Int,name:String,birthday:LocalDate){validProperty=SimpleIntegerProperty(id)varidbyidProperty
valnameProperty=SimpleStringProperty(name)varnamebynameProperty
valbirthdayProperty=SimpleObjectProperty(birthday)varbirthdaybybirthdayProperty
valage:Intget()=Period.between(birthday,LocalDate.now()).years}
Youcanthenbuildanentire"View"containingaTableViewwithasmallcodefootprint.
1.WhyTornadoFX?
4
http://fxexperience.com/controlsfx/http://jfxtras.org/https://github.com/TomasMikula/ReactFXhttps://github.com/ReactiveX/RxJavahttps://github.com/ReactiveX/RxJavaFXhttps://github.com/ReactiveX/RxKotlinhttps://github.com/thomasnield/RxKotlinFXhttps://kotlinlang.org/docs/reference/
-
classMyView:View(){
privatevalpersons=listOf(Person(1,"SamanthaStuart",LocalDate.of(1981,12,4)),Person(2,"TomMarks",LocalDate.of(2001,1,23)),Person(3,"StuartGills",LocalDate.of(1989,5,23)),Person(3,"NicoleWilliams",LocalDate.of(1998,8,11))).observable()
overridevalroot=tableview(persons){column("ID",Person::idProperty)column("Name",Person::nameProperty)column("Birthday",Person::birthdayProperty)column("Age",Person::age)}}
RENDEREDOUTPUT:
Halfofthatcodewasjustinitializingsampledata!IfyouhoneinonjustthepartdeclaringtheTableViewwithfourcolumns(shownbelow),youwillseeittookasimplefunctionalconstructtobuildaTableView.Itwillautomaticallysupporteditstothefieldsaswell.
tableview(persons){column("ID",Person::idProperty)column("Name",Person::nameProperty)column("Birthday",Person::birthdayProperty)column("Age",Person::age)}
Asshownbelow,wecanusethecellFormat()extensionfunctiononaTableColumn,andcreateconditionalformattingfor"Age"valuesthatarelessthan18.
1.WhyTornadoFX?
5
-
tableview{items=personscolumn("ID",Person::idProperty)column("Name",Person::nameProperty)column("Birthday",Person::birthdayProperty)column("Age",Person::age).cellFormat{text=it.toString()style{if(it<18){backgroundColor+=c("#8b0000")textFill=Color.WHITE}}}}
RENDEREDOUTPUT:
ThesedeclarationsarepureKotlincode,andTornadoFXispackedwithexpressivepowerfordozensofcaseslikethis.ThisallowsyoutofocusoncreatingsolutionsratherthanengineeringUIcode.YourJavaFXapplicationswillnotonlybeturnedaroundmorequickly,butalsobemaintainableandevolvable.
1.WhyTornadoFX?
6
-
SettingUpTouseTornadoFX,thereareseveraloptionstosetupthedependencyforyourproject.MainstreambuildautomationtoolslikeGradleandMavenaresupportedandshouldhavenoissuesingettingsetup.
PleasenotethatTornadoFXisaKotlinlibrary,andthereforeyourprojectneedstobeconfiguredtouseKotlin.ForGradleandMavenconfigurations,pleaserefertotheKotlinGradleSetupandKotlinMavenSetupguides.MakesureyourdevelopmentenvironmentorIDEisequippedtoworkwithKotlinandhastheproperpluginsandcompilers.
ThisguidewilluseIntellijIDEAtowalkthroughcertainexamples.IDEAistheIDEofchoicetoworkwithKotlin,althoughEclipsehasapluginaswell.
GradleForGradle,youcansetupthedependencydirectlyfromMavenCentral.Providethedesiredversionnumberforthex.y.zplaceholder.
repositories{mavenCentral()}
//MinimumjvmTargetof1.8neededsinceKotlin1.1compileKotlin{kotlinOptions.jvmTarget=1.8}
dependencies{compile'no.tornado:tornadofx:x.y.z'}
MavenToimportTornadoFXwithMaven,addthefollowingdependencytoyourPOMfile.Providethedesiredversionnumberforthex.y.zplaceholder.
Goesintokotlin-maven-pluginblock:
2.SettingUp
7
http://gradle.org/https://maven.apache.org/https://kotlinlang.org/docs/reference/using-gradle.htmlhttps://kotlinlang.org/docs/reference/using-maven.html
-
1.8
Thenthisgoesintodependenciesblock:
no.tornadotornadofxx.y.z
OtherBuildAutomationSolutions
ForinstructionsonhowtouseTornadoFXwithotherbuildautomationsolutions,pleaserefertothe[TornadoFXpageattheCentralRepository]([http://search.maven.org/#search|gav|1|g%3A"no.tornado](http://search.maven.org/#search|gav|1|g%3A"no.tornado)"ANDa%3A"tornadofx")
ManualImportTomanuallydownloadandimporttheJARfile,gototheTornadoFXreleasepageortheCentralRepository.DownloadtheJARfileandconfigureitintoyourproject.
StartingaTornadoFXApplicationNewerversionsoftheJVMknowhowtostartJavaFXapplicationswithoutamain()method.AJavaFXapplication,andbyextensionaTornadoFXapplication,isanyclassthatextendsjavafx.application.Application.Sincetornadofx.Appextendsjavafx.application.Application,TornadoFXappsarenodifferent.Thereforeyouwouldstarttheappbyreferencingcom.example.app.MyApp,andyoudon'tnecessarilyneedamain()functionunlessyouneedtosupplycommandlinearguments.InthatcaseyouwouldaddapackagelevelmainfunctiontotheMyApp.ktfile:
funmain(args:Array){Application.launch(MyApp::class.java,*args)}
2.SettingUp
8
http://search.maven.org/#search|gav|1|g%3A"no.tornado]%28http://search.maven.org/#search|gav|1|g%3A"no.tornadohttps://github.com/edvin/tornadofx/releaseshttps://search.maven.org
-
Thismainfunctionwouldbecompiledtocom.example.app.MyAppKt-noticetheKtattheend.Whenyoucreateapackagelevelmainfunction,itwillalwayshaveaclassnameofthefullyqualifiedpackage,plusthefilename,appendedwithKt.
Infact,TornadoFXalsocontainsahelpertomakeitevennicertolaunchapplicationsfromamainclassbyacceptingtheappclassasagenerictypeparameter:
funmain(args:Array)=launch(args)
2.SettingUp
9
-
ComponentsJavaFXusesatheatricalanalogytoorganizeanApplicationwithStageandScenecomponents.TornadoFXbuildsonthisbyprovidingView,Controller,andFragmentcomponents.WhiletheStage,andSceneareusedbyTornadoFX,theView,Controller,andFragmentintroducesnewconceptsthatstreamlinedevelopment.Manyofthesecomponentsareautomaticallymaintainedassingletons,andcancommunicatetoeachotherthroughsimpledependencyinjectionsandothermeans.
YoualsohavetheoptiontoutilizeFXMLwhichwillbediscussedmuchlater.Butfirst,letsextendApptocreateanentrypointthatlaunchesaTornadoFXapplication.
AppandViewBasicsTocreateaTornadoFXapplication,youmusthaveatleastoneclassthatextendsApp.AnAppistheentrypointtotheapplicationandspecifiestheinitialView.ItdoesinfactextendJavaFXApplication,butyoudonotnecessarilyneedtospecifyastart()ormain()method.
Butfirst,extendApptocreateyourownimplementationandspecifytheprimaryviewasthefirstconstructorargument.
classMyApp:App(MyView::class)
AViewcontainsdisplaylogicaswellasalayoutofNodes,similartotheJavaFXStage.Itisautomaticallymanagedasasingleton.WhenyoudeclareaViewyoumustspecifyarootpropertywhichcanbeanyNodetype,andthatwillholdtheView'scontent.
InthesameKotlinfileorinanewfile,extendaclassoffofView.OverridetheabstractrootpropertyandassignitVBoxoranyotherNodeyouchoose.
classMyView:View(){overridevalroot=VBox()}
However,wemightwanttopopulatethisVBoxactingastherootcontrol.Usingtheinitializerblock,let'saddaJavaFXButtonandaLabel.Youcanusethe"plusassign"+=operatorstoaddchildren,suchasaButtonandLabel
3.Components
10
https://kotlinlang.org/docs/reference/classes.html#constructors
-
classMyView:View(){overridevalroot=VBox()
init{root+=Button("PressMe")root+=Label("")}}
Whileitisprettyclearwhat'sgoingonfromlookingatthiscode,TornadoFXprovidesabuildersyntaxthatwillstreamlineyourUIcodefurtherandmakeitmucheasiertoreasonabouttheresultingUIjustbylookingatthecode.Wewillgraduallymoveintobuildersyntax,andfinallycoverbuildersinfullinthenextchapter.
Whileweintroduceyoutonewconcepts,youmightsometimesseecodethatisnotusingbestpractices.Wedothistointroduceyougraduallytoconceptsandgiveyouabroaderunderstandingofwhatisgoingonunderthehood.Graduallywewillintroducemorepowerfulconstructstosolvetheproblemathandinabetterway.
Nextwewillseehowtorunthisapplication.
StartingaTornadoFXApplicationNewerversionsoftheJVMknowhowtostartJavaFXapplicationswithoutamain()method.AJavaFXapplication,andbyextensionaTornadoFXapplication,isanyclassthatextendsjavafx.application.Application.Sincetornadofx.Appextendsjavafx.application.Application,TornadoFXappsarenodifferent.Thereforeyouwouldstarttheappbyreferencingcom.example.app.MyApp,andyoudonotnecessarilyneedamain()functionunlessyouneedtosupplycommandlinearguments.InthatcaseyouwouldaddapackagelevelmainfunctiontotheMyApp.ktfile:
funmain(args:Array){Application.launch(MyApp::class.java,*args)}
Thismainfunctionwouldbecompiledtocom.example.app.MyAppKt.NoticetheKtattheend.Whenyoucreateapackagelevelmainfunction,itwillalwayshaveaclassnameofthefullyqualifiedpackage,plusthefilename,appendedwithKt.
ForlaunchingandtestingtheApp,wewilluseIntellijIDEA.NavigatetoRun→EditConfigurations(Figure3.1).
Figure3.1
3.Components
11
-
Clickthegreen"+"signandcreateanewApplicationconfiguration(Figure3.2).
Figure3.2
3.Components
12
-
Specifythenameofyour"Mainclass"whichshouldbeyourAppclass.Youwillalsoneedtospecifythemoduleitresidesin.Givetheconfigurationameaningfulnamesuchas"Launcher".Afterthatclick"OK"(Figure3.3).
Figure3.3
YoucanrunyourTornadoFXapplicationbyselectingRun→Run'Launcher'orwhateveryounamedtheconfiguration(Figure3.4).
Figure3.4
3.Components
13
-
Youshouldnowseeyourapplicationlaunch(Figure3.5)
Figure3.5
Congratulations!Youhavewrittenyourfirst(albeitsimple)TornadoFXapplication.Itmaynotlooklikemuchrightnow,butaswecovermoreofTornadoFX'spowerfulfeatureswewillbecreatinglarge,impressiveuserinterfaceswithlittlecodeinnotime.Butfirstlet'sunderstandalittlebetterwhatishappeningbetweenAppandView.
UnderstandingViewsLet'sdivealittledeeperintohowaViewworksandhowitcanbeused.TakealookattheAppandViewclasseswejustbuilt.
3.Components
14
-
classMyApp:App(MyView::class)
classMyView:View(){overridevalroot=VBox()
init{with(root){this+=Button("PressMe")this+=Label("Waiting")}}}
AViewcontainsahierarchyofJavaFXNodesandisinjectedbynamewhereveritiscalled.InthenextsectionwewilllearnhowtoleveragepowerfulbuilderstocreatetheseNodehierarchiesquickly.ThereisonlyoneinstanceofMyViewmaintainedbyTornadoFX,effectivelymakingitasingleton.TornadoFXalsosupportsscopes,whichcangrouptogetheracollectionofViews,FragmentsandControllersinseparateinstances,resultinginaViewonlybeingasingletoninsidethatscope.ThisisgreatforMultiple-DocumentInterfaceapplicationsandotheradvancedusecases.Thisiscoveredinalaterchapter.
Usinginject()andEmbeddingViewsYoucanalsoinjectoneormoreViewsintoanotherView.BelowweembedaTopViewandBottomViewintoaMasterView.Noteweusetheinject()delegatepropertytolazilyinjecttheTopViewandBottomViewinstances.Thenwecalleach"child"View'sroottoassignthemtotheBorderPane(Figure3.6).
classMasterView:View(){valtopView:TopViewbyinject()valbottomView:BottomViewbyinject()
overridevalroot=borderpane{top=topView.rootbottom=bottomView.root}}
classTopView:View(){overridevalroot=label("TopView")}
classBottomView:View(){overridevalroot=label("BottomView")}
3.Components
15
https://msdn.microsoft.com/en-us/library/aa263481.aspx
-
Figure3.6
IfyouneedViewstocommunicatetoeachother,youcancreateapropertyineachofthe"child"Viewsthatholdsthe"parent"View.
classMasterView:View(){overridevalroot=BorderPane()
valtopView:TopViewbyinject()valbottomView:BottomViewbyinject()
init{with(root){top=topView.rootbottom=bottomView.root}
topView.parent=thisbottomView.parent=this}}
classTopView:View(){overridevalroot=Label("TopView")lateinitvarparent:MasterView}
classBottomView:View(){overridevalroot=Label("BottomView")lateinitvarparent:MasterView}
MoretypicallyyouwoulduseaControlleroraViewModeltocommunicatebetweenviews,andwewillvisitthistopiclater.
InjectionUsingfind()Theinject()delegatewilllazilyassignagivencomponenttoaproperty.Thefirsttimethatcomponentiscallediswhenitwillberetrieved.Alternatively,insteadofusingtheinject()delegateyoucanusethefind()functiontoretrieveasingletoninstanceofaVieworothercomponents.
3.Components
16
-
classMasterView:View(){overridevalroot=BorderPane()
valtopView=find(TopView::class)valbottomView=find(BottomView::class)
init{with(root){top=topView.rootbottom=bottomView.root}}}
classTopView:View(){overridevalroot=Label("TopView")}
classBottomView:View(){overridevalroot=Label("BottomView")}
Youcanuseeitherfind()orinject(),butusinginject()delegatesisthepreferredmeanstoperformdependencyinjection.
IntroductiontoBuildersWhilewewillcoverbuildersmoreindepthinthenextchapter,itistimetorevealthattheaboveexamplecanbewritteninamuchmoreconciseandexpressivesyntax:
classMasterView:View(){overridevalroot=borderpane{top(TopView::class)bottom(BottomView::class)}}
InsteadofinjectingtheTopViewandBottomViewandthenassigningtheirrespectiverootnodestotheBorderPanestopandbottomproperty,wespecifytheBorderPanewiththebuildersyntax(alllowercase)andthendeclarativelytellTornadoFXtopullinthetwosubviewsandassignthemtothetopandbottompropertiesautomatically.Hopefullyyouagreethisismuchmoreexpressive,withalotlessboilerplate.ThisisoneofthemostimportantprinciplesTornadoFXtriestoliveby:Reduceboilerplateandincreasereadability.Theendresultisoftenlesscodeandlessbugs.
3.Components
17
-
ControllersInmanycases,itisconsideredagoodpracticetoseparateaUIintothreedistinctparts:
1. Model-Thebusinesscodelayerthatholdscorelogicanddata2. View-Thevisualdisplaywithvariousinputandoutputcontrols3. Controller-The"middleman"mediatingeventsbetweentheModelandtheView
ThereareotherflavorsofMVClikeMVVMandMVP,allofwhichcanbeleveragedinTornadoFX.
WhileyoucouldputalllogicfromtheModelandControllerrightintotheview,itisoftencleanertoseparatethesethreepiecesdistinctlytomaximizereusability.OnecommonlyusedpatterntoaccomplishthisistheMVCpattern.InTornadoFX,aControllercanbeinjectedtosupportaView.
Hereisasimpleexample.CreateasimpleViewwithaTextFieldwhosevalueiswrittentoa"database"whenaButtonisclicked.WecaninjectaControllerthathandlesinteractingwiththemodelthatwritestothedatabase.Sincethisexampleissimplified,therewillbenodatabasebutaprintedmessagewillserveasaplaceholder(Figure3.7).
classMyView:View(){valcontroller:MyControllerbyinject()varinputField:TextFieldbysingleAssign()
overridevalroot=vbox{label("Input")inputField=textfield()button("Commit"){action{controller.writeToDb(inputField.text)inputField.clear()}}}}
classMyController:Controller(){funwriteToDb(inputValue:String){println("Writing$inputValuetodatabase!")}}
Figure3.7
3.Components
18
-
WhenwebuildtheUI,wemakesuretoaddareferencetotheinputFieldsothatitcanbereferencesfromtheonClickeventhandlerofthe"Commit"buttonlater.Whenthe"Commit"buttonisclicked,youwillseetheControllerprintsalinetotheconsole.
WritingAlphatodatabase!
Itisimportanttonotethatwhiletheaboveworks,andmayevenlookprettygood,itisagoodpracticetoavoidreferencingotherUIelementsdirectly.YourcodewillbemucheasiertorefactorifyoubindyourUIelementstopropertiesandmanipulatethepropertiesinstead.WewillintroducetheViewModellater,whichprovideseveneasierwaystodealwiththistypeofinteraction.
YoucanalsouseControllerstoprovidedatatoaView(Figure3.8).
classMyView:View(){valcontroller:MyControllerbyinject()
overridevalroot=vbox{label("Myitems")listview(controller.values)}}
classMyController:Controller(){valvalues=FXCollections.observableArrayList("Alpha","Beta","Gamma","Delta")}
Figure3.8
3.Components
19
-
TheVBoxcontainsaLabelandaListView,andtheitemspropertyoftheListViewisassignedtothevaluespropertyofourController.
Whethertheyarereadingorwritingdata,Controllerscanhavelong-runningtasksandshouldnotperformworkontheJavaFXthread.YouwilllearnhowtoeasilyoffloadworktoaworkerthreadusingtherunAsyncconstructlaterinthischapter.
Longrunningtasks
Wheneveryoucallafunctioninacontrolleryouneedtodetermineifthatfunctionreturnsimmediatelyorifitperformspotentiallylong-runningtasks.IfyoucallafunctionontheJavaFXApplicationThread,theUIwillbeunresponsiveuntilthecallcompletes.UnresponsiveUI'sisakillerforuseracceptance,somakesurethatyourunexpensiveoperationsinthebackground.TornadoFXprovidestherunAsyncfunctiontohelpwiththis.
CodeplacedinsidearunAsyncblockwillruninthebackground.IftheresultofthebackgroundcallshouldupdateyourUI,youmustmakesurethatyouapplythechangesontheJavaFXApplicationThread.Theuiblockdoesexactlythat.
3.Components
20
-
valtextfield=textfield()button("Updatetext"){action{runAsync{myController.loadText()}ui{loadedText->textfield.text=loadedText}}}
Whenthebuttonisclicked,theactioninsidetheactionbuilder(whichdelegatestheActionEventtosetActionmethod)isrun.ItmakesacallouttomyController.loadText()andappliestheresulttothetextpropertyofthetextfieldwhenitreturns.TheUIstaysresponsivewhilethecontrollerfunctionruns.
Underthecovers,runAsynccreatesJavaFXTaskobjects,andspinsoffaseparatethreadtorunyourcallinsidetheTask.YoucanassignthisTasktoavariableandbindittoaUItoshowprogresswhileyouroperationisrunning.
Infact,thisissocommonthatthereisalsoandefaultViewModelcalledTaskStatuswhichcontainsobservablevaluesforrunning,message,title,andprogress.YoucansupplytherunAsynccallwithaspecificinstanceoftheTaskStatusobject,orusethedefault.
TheTornadoFXsourcesincludesanexampleusageofthisintheAsyncProgressApp.ktfile.
ThereisalsoaversionofrunAsynccalledrunAsyncWithProgresswhichwillcoverthecurrentnodewithaprogressindicatorwhilethelongrunningoperationruns.
singleAssign()PropertyDelegate
IntheexampleaboveweinitializedtheinputFieldpropertywiththesingleAssigndelegate.Ifyouwanttoguaranteethatavalueisonlyassignedonce,youcanusethesingleAssign()delegateinsteadofthelateinitkeywordfromKotlin.Thiswillcauseasecondassignmenttothrowanerror,anditwillalsoerrorwhenitisprematurelyaccessedbeforeitisassigned.
YoucanlookupmoreaboutsingleAssign()indetailinAppendixA1,butknowfornowitguaranteesavarcanonlybeassignedonce.Itisalsothreadsafeandhelpsmitigateissueswithmutability.
Fragment
3.Components
21
-
AnyViewyoucreateisasingleton,whichmeansyoutypicallyuseitinonlyoneplaceatatime.ThereasonforthisisthattherootnodeoftheViewcanonlyhaveasingleparentinaJavaFXapplication.Ifyouassignitanotherparent,itwilldisappearfromitspreviousparent.
However,ifyouwouldliketocreateapieceofUIthatisshort-livedorcanbeusedinmultipleplaces,considerusingaFragment.AFragmentisaspecialtypeofViewthatcanhavemultipleinstances.TheyareparticularlyusefulforpopupsoraspiecesofalargerUI(suchasListCells,whichwelookatviatheListCellFragmentlater).
BothViewandFragmentsupportopenModal(),openWindow()andopenInternalWindow()thatwillopentherootnodeinaseparateWindow.
classMyView:View(){overridevalroot=vbox{button("PressMe"){action{find(MyFragment::class).openModal(stageStyle=StageStyle.UTILITY)}}}}
classMyFragment:Fragment(){overridevalroot=label("Thisisapopup")}
YoucanpassoptionalargumentstoopenModal()aswelltomodifyafewofitsbehaviors.
OptionalArgumentsforopenModal()
Argument Type Description
stageStyle StageStyle DefinesoneofthepossibleenumstylesforStage.Default:StageStyle.DECORATED
modality ModalityDefinesoneofthepossibleenummodalitytypesforStage.Default:Modality.APPLICATION_MODAL
escapeClosesWindow Boolean SetstheESCkeytocallcloseModal().Default:true
owner Window SpecifytheownerWindowforthisStage`
block Boolean BlockUIexecutionuntiltheWindowcloses.Default:false
InternalWindow
3.Components
22
-
WhileopenModalopensinanewStage,openInternalWindowopensoverthecurrentrootnode,oranyothernodeifyouspecifyit:
button("Openeditor"){action{openInternalWindow(Editor::class)}}
Figure3.9
AgoodusecasefortheinternalwindowisforsinglestageenvironmentslikeJPro,orifyouwanttocustomizethewindowtrimtomakethewindowappearmoreinlinewiththedesignofyourapplication.TheInternalWindowcanbestyledwithCSS.TakealookattheInternalWindow.Stylesclassformoreinformationaboutstyleableproperties.
TheinternalwindowAPIdiffersfrommodal/windowinoneimportantaspect.Sincethewindowopensoveranexistingnode,youtypicallycallopenInternalWindow()fromwithintheViewyouwantittoopenontopof.YousupplytheViewyouwanttoshow,andyoucanoptionallysupplywhatnodetoopenoverviatheownerparameter.
OptionalArgumentsforopenInternalWindow()
3.Components
23
-
Argument Type Description
view UIComponent Thecomponentwillbethecontentofthenewwindow
view KClass Alternatively,youcansupplytheclassoftheviewinsteadofaninstance
icon Node Optionalwindowicon
scope Scope Ifyouspecifytheviewclass,youcanalsospecifythescopeusedtofetchtheview
modal BooleanDefinesifthecoveringnodeshouldbedisabledwhiletheinternalwindowisactive.Default:true
escapeClosesWindow Boolean SetstheESCkeytocallclose().Default:true
owner NodeSpecifytheownerNodeforthiswindow.Thewindowwillbydefaultcovertherootnodeofthisview.`
ClosingmodalwindowsAnyComponentopenedusingopenModal(),openWindow()oropenInternalWindow()canbeclosedbycallingcloseModal().ItisalsopossibletogettotheInternalWindowinstancedirectlyifneededusingfindParentOfType(InternalWindow::class).
ReplacingViewsandDockingEventsWithTornadoFX,iseasytoswapyourcurrentViewwithanotherViewusingreplaceWith(),andoptionallyaddatransition.Intheexamplebelow,aButtononeachViewwillswitchtotheotherview,whichcanbeMyView1orMyView2(Figure3.10).
3.Components
24
-
classMyView1:View(){overridevalroot=vbox{button("GotoMyView2"){action{replaceWith(MyView2::class)}}}}
classMyView2:View(){overridevalroot=vbox{button("GotoMyView1"){action{replaceWith(MyView1::class)}}}}
Figure3.10
YoualsohavetheoptiontospecifyaspiffyanimationforthetransitionbetweenthetwoViews.
replaceWith(MyView1::class,ViewTransition.Slide(0.3.seconds,Direction.LEFT)
ThisworksbyreplacingtherootNodeongivenViewwithanotherView'sroot.TherearetwofunctionsyoucanoverrideonViewtoleveragewhenaView'srootNodeisconnectedtoaparent(onDock()),andwhenitisdisconnected(onUndock()).Youcanleveragethesetwoeventstoconnectand"cleanup"wheneveraViewcomesinorfallsout.YouwillnoticerunningthecodebelowthatwheneveraViewisswapped,itwillundockthatpreviousViewanddockthenewone.Youcanleveragethesetwoeventstomanageinitializationanddisposaltasks.
3.Components
25
-
classMyView1:View(){overridevalroot=vbox{button("GotoMyView2"){action{replaceWith(MyView2::class)}}}
overridefunonDock(){println("DockingMyView1!")}
overridefunonUndock(){println("UndockingMyView1!")}}
classMyView2:View(){overridevalroot=vbox{button("GotoMyView1"){action{replaceWith(MyView1::class)}}}
overridefunonDock(){println("DockingMyView2!")}overridefunonUndock(){println("UndockingMyView2!")}}
PassingparameterstoviewsThebestwaytopassinformationbetweenviewsisoftenaninjectedViewModel.Evenso,itcanstillbeconvenienttobeabletopassparameterstoothercomponents.ThefindandinjectfunctionssupportsvarargsofPairwhichcanbeusedforjustthispurpose.Consideracustomerlistthatopensacustomereditorfortheselectedcustomer.Theactiontoeditacustomermightlooklikethis:
funeditCustomer(customer:Customer){find(mapOf(CustomerEditor::customertocustomer).openWindow())}
3.Components
26
-
Theparametersarepassedasamap,wherethekeyisthepropertyintheviewandthevalueiswhateveryouwantthepropertytobe.ThisgivesyouatypesafewayofconfiguringparametersforthetargetView.
HereweusetheKotlintosyntaxtocreatetheparameter.ThiscouldalsohavebeenwrittenasPair(CustomerEditor::customer,customer)ifyouprefer.Theeditorcannowaccesstheparameterlikethis:
classCustomerEditor:Fragment(){valcustomer:Customerbyparam()
}
Ifyouwanttoinspecttheparametersinsteadofblindlyrelyingonthemtobeavailable,youcaneitherdeclarethemasnullableorconsulttheparamsmap:
classCustomerEditor:Fragment(){init{valcustomer=params["customer"]as?Customerif(customer!=null){...}}}
Ifyoudon'tcareabouttypesafetyyoucanalsopassparametersasmapOf("customer"tocustomer),butthenyoumissoutonautomaticrefactoringifyourenameapropertyinthetargetview.
AccessingtheprimarystageViewhasapropertycalledprimaryStagethatallowsyoutomanipulatepropertiesoftheStagebackingit,suchaswindowsizeforexample.AnyVieworFragmentthatwereopenedviaopenModalwillalsohaveamodalStagepropertyavailable.
AccessingthesceneSometimesitisnecessarytogetaholdofthecurrentscenefromwithinaVieworFragment.Thiscanbeachievedwithroot.scene,orifyouarewithinatypesafebuilder,thereisanevenshorterway,justusescene.
3.Components
27
-
AccessingresourcesLot'sofJavaFXAPIstakesresourcesasanURLorthetoExternalFormofanURL.Toretrievearesourceurlonewouldtypicallywritesomethinglike:
valmyAudioClip=AudioClip(MyView::class.java.getResource("mysound.wav").toExternalForm())
EveryComponenthasaresourcesobjectwhichcanretrievetheexternalformurlofaresourcelikethis:
valmyAudiClip=AudioClip(resources["mysound.wav"])
IfyouneedanactualURLitcanberetrievedlikethis:
valmyResourceURL=resources.url("mysound.wav")
TheresourceshelperalsohasseveralotherhelpfulfunctionstohelpyouturnfilesrelativetotheComponentintoanobjectofthetypeyouneed:
valmyJsonObject=resources.json("myobject.json")valmyJsonArray=resources.jsonArray("myarray.json")valmyStream=resources.stream("somefile")
It'sworthmentioningthatthejsonandjsonArrayfunctionsarealsoavailableonInputStreamobjects.
ResourcesarerelativetotheComponentbutyoucanalsoretrievearesourcebyit'sfullpath,startingwitha/.
ShortcutsandkeycombinationsforactionsYoucanfireactionswhencertainkeycombinationsaretyped.Thisisdonewiththeshortcutfunction:
shortcut(KeyCombination.valueOf("Ctrl+Y")){doSomething()}
3.Components
28
-
Thereisalsoastringversionoftheshortcutfunctionthatdoesthesamebutislessverbose:
shortcut("Ctrl+Y")){doSomething()}
Youcanalsoaddshortcutstobuttonactionsdirectly:
button("Save"){action{doSave()}shortcut("Ctrl+S")}
TouchSupport
JavaFXsupportstouchoutofthebox,andfornowtheonlyplaceweneededtoimproveitwastohandleshortpressandlongpressinamoreconvenientway.Itconsistsoftwofunctionssimilartoaction,whichcanbeconfiguredonanyNode:
shortpress{println("Activatedonshortpress")}longpress{println("Activatedonlongpress")}
Bothfunctionsacceptsaconsumeparameterwhichbydefaultisfalse.Settingittotruewillpreventeventbubblingforthepressevent.Thelongpressfunctionadditionallysupportsathresholdparameterwhichisusedtodeterminewhenalongpresshasaccured.Itis700.millisbydefault.
SummaryTornadoFXisfilledwithsimple,streamlined,andpowerfulinjectiontoolstomanageViewsandControllers.ItalsostreamlinesdialogsandothersmallUIpiecesusingFragment.Whiletheapplicationswebuiltsofarareprettysimple,hopefullyyouappreciatethesimplifiedconceptsTornadoFXintroducestoJavaFX.InthenextchapterwewillcoverwhatisarguablythemostpowerfulfeatureofTornadoFX:Type-SafeBuilders.
3.Components
29
-
BasicControlsOneofthemostexcitingfeaturesofTornadoFXaretheType-SafeBuilders.ConfiguringandlayingoutcontrolsforcomplexUI'scanbeverboseanddifficult,andthecodecanquicklybecomemessytomaintain.Fortunately,youcanuseapowerfulclosurepatternpioneeredbyGroovytocreatestructuredUIlayoutswithpureandsimpleKotlincode.
WhilewewilllearnhowtoapplyFXMLlater,youmayfindbuilderstobeanexpressive,robustwaytocreatecomplexUI'sinafractionofthetime.Therearenoconfigurationfilesorcompilermagictricks,andbuildersaredonewithpureKotlincode.Thenextseveralchapterswilldividethebuildersintoseparatecategoriesofcontrols.Alongtheway,youwillgraduallybuildmorecomplexUI'sbyintegratingthesebuilderstogether.
Butfirst,let'scoverhowbuildersactuallywork.
HowBuildersWorkKotlin'sstandardlibrarycomeswithahandfulofhelpful"block"functionstotargetitemsofanytypeT.Thereisthewith()function,whichallowsyoutowritecodeagainstanitemasifyouwererightinsideofitsclass.
classMyView:View(){
overridevalroot=VBox()
init{with(root){this+=Button("PressMe")}}}
Intheaboveexample,thewith()functionacceptstherootasanargument.Thefollowingclosureargumentmanipulatesrootdirectlybyreferringtoitasthis,whichissafelyinterpretedasaVBox.AButtonwasaddedtotheVBoxbycallingit'splusAssign()extendedoperator.
Alternatively,everytypeinKotlinhasanapply()function.Thisisalmostthesamefunctionalityaswith()butitisactuallyanextendedhigher-orderfunction.
4.BasicControls
30
https://kotlinlang.org/docs/reference/type-safe-builders.htmlhttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin/with.htmlhttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html
-
classMyView:View(){
overridevalroot=VBox()
init{root.apply{this+=Button("PressMe")}}}
Bothwith()andapply()accomplishasimilartask.Theysafelyinterpretthetypetheyaretargetingandallowmanipulationstobedonetoit.However,with()returnsthelaststatementwithinthelambda,whereasapply()doesinfactreturntheitemitwastargeting.Therefore,ifyoucallapply()onaButtontomanipulatesay,itsfontcolorandaction,itishelpfultheButtonreturnsitselfsoastonotbreakthedeclarationflow.
classMyView:View(){
overridevalroot=VBox()
init{with(root){this+=Button("PressMe").apply{textFill=Color.REDaction{println("Buttonpressed!")}}}}}
Thebasicconceptsofhowbuildersworkareexpressedabove,andtherearethreetasksbeingdone:
1. AButtoniscreated2. TheButtonismodified3. TheButtonisaddedtoits"parent",whichisaVBox
WhendeclaringanyNode,thesethreestepsaresocommonthatTornadoFXstreamlinesthemforyouusingstrategicallyplacedextensionfunctions,suchasbutton()asshownbelow.
4.BasicControls
31
-
classMyView:View(){
overridevalroot=VBox()
init{with(root){button("PressMe"){textFill=Color.REDaction{println("Buttonpressed!")}}}}}
Whilethislooksmuchcleaner,youmightbewondering:"Howdidwejustgetridofthethis+=andapply()functioncall?Andwhyareweusingafunctioncalledbutton()insteadofanactualButton?"Wewillnotgotoodeeponhowthisisdone,andyoucanalwaysdigintothesourcecodeifyouarecurious.Butessentially,theVBox(oranytargetablecomponent)hasanextensionfunctioncalledbutton().ItacceptsatextargumentandanoptionalclosuretargetingaButtonitwillinstantiate.Whenthisfunctioniscalled,itwillcreateaButtonwiththespecifiedtext,applytheclosuretoit,addittotheVBoxitwascalledon,andthenreturnit.
Takingthisefficiencyfurther,youcanoverridetherootinaView,butassignitabuilderfunctionandavoidneedinganyinitandwith()blocks.
classMyView:View(){
overridevalroot=vbox{button("PressMe"){textFill=Color.REDaction{println("Buttonpressed!")}}}}
Thebuilderpatternbecomesespeciallypowerfulwhenyoustartnestingcontrolsintoothercontrols.Usingthesebuilderextensionfunctions,youcaneasilypopulateandnestmultipleHBoxinstancesintoaVBox,andcreateUIcodethatisclearlystructured(Figure4.1).
4.BasicControls
32
https://github.com/edvin/tornadofx/blob/master/src/main/java/tornadofx/Controls.kt#L234
-
classMyView:View(){
overridevalroot=vbox{hbox{label("FirstName")textfield()}hbox{label("LastName")textfield()}button("LOGIN"){useMaxWidth=true}}}
Figure4.1
AlsonotewewilllearnaboutTornadoFX'sproprietaryFormlater,whichwillmakesimpleinputUI'slikethisevensimplertocode.
IfyouneedtosavereferencestocontrolssuchastheTextFields,youcansavethemtovariablesorpropertiessincethefunctionsreturntheproducedcontrols.ItisrecommendyouusethesingleAssign()delegatestoensurethepropertiesareonlyassignedonce.
4.BasicControls
33
-
classMyView:View(){
varfirstNameField:TextFieldbysingleAssign()varlastNameField:TextFieldbysingleAssign()
overridevalroot=vbox{hbox{label("FirstName")firstNameField=textfield()}hbox{label("LastName")lastNameField=textfield()}button("LOGIN"){useMaxWidth=trueaction{println("Logginginas${firstNameField.text}${lastNameField.text}")}}}
}
Notethatnon-builderextensionfunctionsandpropertieshavebeenaddedtodifferentcontrolsaswell.TheuseMaxWidthisanextendedpropertyforNode,anditsetstheNodetooccupythemaximumwidthallowed.Wewillseemoreofthesehelpfulextensionsthroughoutthenextfewchapters.
Inthecomingchapters,wewillcovereachcorrespondingbuilderforeachJavaFXcontrol.Withtheconceptsunderstoodabove,youcanreadaboutthesenextchaptersstarttofinishorasareference.
BuildersforBasicControlsTherestofthischapterwillcoverbuildersforcommonJavaFXcontrolslikeButton,Label,andTextField.Thenextchapterwillcoverbuildersfordata-drivencontrolslikeListView,TableView,andTreeTableView.
Button
ForanyPane,youcancallitsbutton()extensionfunctiontoaddaButtontoit.YoucanoptionallypassatextargumentandaButton.()->Unitlambdatomodifyitsproperties.
4.BasicControls
34
-
WithinaPane,thiswilladdaButtonwithredtextandprint"Buttonpressed!"everytimeitisclicked(Figure4.2)
button("PressMe"){textFill=Color.REDaction{println("Buttonpressed!")}}
Figure4.2
Label
Youcancallthelabel()extensionfunctiontoaddaLabeltoagivenPane.Optionallyyoucanprovideatext(oftypeStringorProperty),agraphic(oftypeNodeorObjectProperty)andaLabel.()->Unitlambdatomodifyitsproperties(Figure4.3).
label("Loremipsum",circle(10,10,5)){textFill=Color.BLUE}
Figure4.3
TextField
ForanyPaneyoucanaddaTextFieldbycallingitstextfield()extensionfunction(Figure4.4).
textfield()
Figure4.4
4.BasicControls
35
-
YoucanoptionallyprovideinitialtextaswellasaclosuretomanipulatetheTextField.Forexample,wecanaddalistenertoitstextProperty()andprintitsvalueeverytimeitchanges(Figure4.5).
textfield("Inputsomething"){textProperty().addListener{obs,old,new->println("Youtyped:"+new)}}
Figure4.6
PasswordField
IfyouneedaTextFieldtotakesensitiveinformation,youmightwanttoconsideraPasswordFieldinstead.Itwillshowanonymouscharacterstoprotectfrompryingeyes.Youcanalsoprovideaninitialpasswordasanargumentandablocktomanipulateit(Figure4.7).
passwordfield("my_password"){requestFocus()}
Figure4.7
CheckBox
YoucancreateaCheckBoxtoquicklycreateatrue/falsestatecontrolandoptionallymanipulateitwithablock(Figure4.8).
4.BasicControls
36
-
checkbox("AdminMode"){action{println(isSelected)}}
Noticethattheactionblockiswrappedinsidethecheckboxsoyoucanaccessit'sisSelectedproperty.Ifyoudon'tneedaccesstothepropertiesoftheCheckBoxyoucouldhavewrittencheckbox("AdminMode").action{}.
Figure4.9
YoucanalsoprovideaPropertythatwillbindtoitsselectionstate.
valbooleanProperty=SimpleBooleanProperty()
checkbox("AdminMode",booleanProperty).action{println(isSelected)}
ComboBox
AComboBoxisadropdowncontrolthatallowsafixedsetofvaluestobeselectedfrom(Figure4.10).
valtexasCities=FXCollections.observableArrayList("Austin","Dallas","Midland","SanAntonio","FortWorth")
combobox{items=texasCities}
Figure4.10
4.BasicControls
37
-
Youdonotneedtospecifythegenerictypeifyoudeclarethevaluesasanargument.
valtexasCities=FXCollections.observableArrayList("Austin","Dallas","Midland","SanAntonio","FortWorth")
combobox(values=texasCities)
YoucanalsospecifyaPropertytobeboundtotheselectedvalue.
valtexasCities=FXCollections.observableArrayList("Austin","Dallas","Midland","SanAntonio","FortWorth")
valselectedCity=SimpleStringProperty()
combobox(selectedCity,texasCities)
ToggleButton
AToggleButtonisabuttonthatexpressesatrue/falsestatedependingonitsselectionstate(Figure4.11).
togglebutton("OFF"){action{text=if(isSelected)"ON"else"OFF"}}
4.BasicControls
38
-
PerhapsamoreidomaticwaytocontrolthebuttontextwouldbetouseaStringBindingboundtothetextProperty:
togglebutton{valstateText=selectedProperty().stringBinding{if(it==true)"ON"else"OFF"}textProperty().bind(stateText)}
Figure4.11
YoucanoptionallypassaToggleGrouptothetogglebutton()function.ThiswillensureallToggleButtonsinthatToggleGroupcanonlyhaveoneselectedatatime(Figure4.12).
classMyView:View(){
privatevaltoggleGroup=ToggleGroup()
overridevalroot=hbox{togglebutton("YES",toggleGroup)togglebutton("NO",toggleGroup)togglebutton("MAYBE",toggleGroup)}}
Figure4.12
RadioButton
ARadioButtonisthesamefunctionalityasaToggleButtonbutwithadifferentvisualstyle.Whenitisselected,it"fills"inacircularcontrol(Figure4.13).
radiobutton("PowerUserMode"){action{println("PowerUserMode:$isSelected")}}
4.BasicControls
39
-
Figure4.13
AlsoliketheToggleButton,youcansetaRadioButtontobeincludedinaToggleGroupsothatonlyoneiteminthatgroupcanbeselectedatatime(Figure4.14).
classMyView:View(){
privatevaltoggleGroup=ToggleGroup()
overridevalroot=vbox{radiobutton("Employee",toggleGroup)radiobutton("Contractor",toggleGroup)radiobutton("Intern",toggleGroup)}}
Figure4.14
DatePicker
TheDatePickerisasimpletodeclare.Itallowsyoutochooseadatefromapopoutcalendarcontrol.Youcanoptionallyprovideablocktomanipulateit(Figure4.15).
datepicker{value=LocalDate.now()}
Figure4.15
4.BasicControls
40
-
YoucanalsoprovideaPropertyasanargumenttobindtoitsvalue.
valdateProperty=SimpleObjectProperty()
datepicker(dateProperty){value=LocalDate.now()}
TextArea
TheTextAreaallowsyouinputmultilinefreeformtext.Youcanoptionallyprovidetheinitialtextvalueaswellasablocktomanipulateitondeclaration(Figure4.16).
textarea("Typememohere"){selectAll()}
Figure4.16
4.BasicControls
41
-
ProgressBar
AProgressBarvisualizesprogresstowardscompletionofaprocess.YoucanoptionallyprovideaninitialDoublevaluelessthanorequalto1.0indicatingpercentageofcompletion(Figure4.17).
progressbar(0.5)
Figure4.17
Hereisamoredynamicexamplesimulatingprogressoverashortperiodoftime.
progressbar(){thread{for(iin1..100){Platform.runLater{progress=i.toDouble()/100.0}Thread.sleep(100)}}}
YoucanalsopassaPropertythatwillbindtheprogresstoitsvalueaswellasablocktomanipulatetheProgressBar.
4.BasicControls
42
-
progressbar(completion){progressProperty().addListener{obsVal,old,new->print("VALUE:$new")}}
ProgressIndicator
AProgressIndicatorisfunctionallyidenticaltoaProgressBarbutusesafillingcircleinsteadofabar(Figure4.18).
progressindicator{thread{for(iin1..100){Platform.runLater{progress=i.toDouble()/100.0}Thread.sleep(100)}}}
Figure4.18
JustliketheProgressBaryoucanprovideaPropertyand/orablockasoptionalarguments(Figure4.19).
valcompletion=SimpleObjectProperty(0.0)progressindicator(completion)
ImageView
Youcanembedanimageusingimageview().
imageview("tornado.jpg")
Figure4.19
4.BasicControls
43
-
Likemostothercontrols,youcanuseablocktomodifyitsattributes(Figure4.20).
imageview("tornado.jpg"){scaleX=.50scaleY=.50}
Figure4.20
4.BasicControls
44
-
ScrollPane
YoucanembedacontrolinsideaScrollPanetomakeitscrollable.Whentheavailableareabecomessmallerthanthecontrol,scrollbarswillappeartonavigatethecontrol'sarea.
Forinstance,youcanwrapanImageViewinsideaScrollPane(Figure4.21).
scrollpane{imageview("tornado.jpg")}
Figure4.21
4.BasicControls
45
-
KeepinmindthatmanycontrolslikeTableViewandTreeTableViewalreadyhavescrollbarsonthem,sowrappingtheminaScrollPaneisnotnecessary(Figure4.22).
Hyperlink
YoucancreateaHyperlinkcontroltomimicthebehaviorofatypicalhyperlinktoafile,awebsite,orsimplyperformanaction.
hyperlink("OpenFile").action{println("Openingfile...")}
Figure4.22
Text
YoucanaddasimplepieceofTextwithformattedproperties.ThiscontrolissimplerandrawerthanaLabel,andparagraphscanbeseparatedusing\ncharacters(Figure4.23).
4.BasicControls
46
-
text("Veni\nVidi\nVici"){fill=Color.PURPLEfont=Font(20.0)}
Figure4.23
TextFlow
Ifyouneedtoconcatenatemultiplepiecesoftextwithdifferentformats,theTextFlowcontrolcanbehelpful(Figure4.24).
textflow{text("Tornado"){fill=Color.PURPLEfont=Font(20.0)}text("FX"){fill=Color.ORANGEfont=Font(28.0)}}
Figure4.24
YoucanaddanyNodetothetextflow,includingimages,usingthestandardbuilderfunctions.
Tooltips
InsideanyNodeyoucanspecifyaTooltipviathetooltip()function(Figure4.25).
4.BasicControls
47
-
button("Commit"){tooltip("Writesinputtothedatabase")}
Figure4.25
Likemostotherbuilders,youcanprovideaclosuretocustomizetheTooltipitself.
button("Commit"){tooltip("Writesinputtothedatabase"){font=Font.font("Verdana")}}
Therearemanyotherbuildercontrols,andthemaintainersofTornadoFXhavestrivedtocreateabuilderforeveryJavaFXcontrol.Ifyouneedsomethingthatisnotcoveredhere,useGoogletoseeifitsincludedinJavaFX.ChancesareifacontrolisavailableinJavaFX,thereisabuilderwiththesamenameinTornadoFX.
SUMMARY
InthischapterwelearnedaboutTornadoFXbuildersandhowtheyworksimplybyusingKotlinextensionfunctions.WealsocoveredbuildersforbasiccontrolslikeButton,TextFieldandImageView.Inthecomingchapterswewilllearnaboutbuildersfortables,layouts,menus,charts,andothercontrols.Asyouwillsee,combiningallthesebuilderstogethercreatesapowerfulwaytoexpresscomplexUI'swithverystructuredandminimalcode.
ThesearenottheonlycontrolbuildersintheTornadoFXAPI,andthisguidedoesitsbesttokeepup.AlwayschecktheTornadoFXGitHubtoseethelatestbuildersandfunctionalitiesavailable,andfileanissueifyouseeanymissing.
4.BasicControls
48
https://github.com/edvin/tornadofx
-
4.BasicControls
49
-
DataControlsAnysignificantapplicationworkswithdata,andprovidingameansforuserstoview,manipulate,andmodifydataisnotatrivialtaskforuserinterfacedevelopment.Fortunately,TornadoFXstreamlinesmanyJavaFXdatacontrolssuchasListView,TableView,TreeView,andTreeTableView.Thesecontrolscanbecumbersometosetupinapurelyobject-orientedway.Butusingbuildersthroughfunctionaldeclarations,wecancodeallthesecontrolsinamuchmorestreamlinedway.
ListView
AListViewissimilartoaComboBoxbutitdisplaysallitemswithinaScrollViewandhastheoptionofallowingmultipleselections,asshowninFigure5.1
listview{items.add("Alpha")items.add("Beta")items.add("Gamma")items.add("Delta")items.add("Epsilon")selectionModel.selectionMode=SelectionMode.MULTIPLE}
Figure5.1
YoucanalsoprovideitanObservableListofitemsupfrontandomitthetypedeclarationsinceitcanbeinferred.UsinganObservableListalsohasthebenefitthatchangestothelistwillautomaticallybereflectedintheListView.
5.DataControls
50
-
valgreekLetters=listOf("Alpha","Beta","Gamma","Delta","Epsilon").observable()
listview(greekLetters){selectionModel.selectionMode=SelectionMode.MULTIPLE}
Likemostdatacontrols,keepinmindthatbydefault,theListViewwillcalltoString()torenderthetextforeachiteminyourdomainclass.Torenderanythingelse,youwillneedtocreateyourowncustomcellformatting.
CustomCellFormattinginListView
EventhoughthedefaultlookofaListViewisratherboring(becauseitcallstoString()andrendersitastext)youcanmodifyitsothateverycellisacustomNodeofyourchoosing.BycallingcellCache(),TornadoFXprovidesaconvenientwaytooverridewhatkindofNodeisreturnedforeachiteminyourlist(Figure5.2).
5.DataControls
51
-
classMyView:View(){
valpersons=listOf(Person("JohnMarlow",LocalDate.of(1982,11,2)),Person("SamanthaJames",LocalDate.of(1973,2,4))).observable()
overridevalroot=listview(persons){cellFormat{graphic=cache{form{fieldset{field("Name"){label(it.name)}field("Birthday"){label(it.birthday.toString())}label("${it.age}yearsold"){alignment=Pos.CENTER_RIGHTstyle{fontSize=22.pxfontWeight=FontWeight.BOLD}}}}}}}}
classPerson(valname:String,valbirthday:LocalDate){valage:Intget()=Period.between(birthday,LocalDate.now()).years}
5.DataControls
52
-
Figure5.2-AcustomcellrenderingforListView
ThecellFormatfunctionletsyouconfigurethetextand/orgraphicpropertyofthecellwheneveritcomesintoviewonthescreen.Thecellsthemselvesarereused,butwhenevertheListViewasksthecelltoupdateit'scontent,thecellFormatfunctioniscalled.Inourexampleweonlyassigntographic,butifyoujustwanttochangethestringrepresentationyoushouldassignittotext.Itiscompletelylegitimatetoassignittobothtextandgraphic.ThevalueswillautomaticallybeclearedbythecellFormatfunctionwhenacertainlistcellisnotshowinganactiveitem.
Notethatassigningnewnodestothegraphicpropertyeverytimethelistcellisaskedtoupdatecanbeexpensive.Itmightbefineformanyusecases,butforheavynodegraphs,ornodegraphswhereyouutilizebindingtowardstheuicomponentsinsidethecell,youshouldcachetheresultingnodesothenodegraphwillonlybecreatedoncepernode.Thisisdoneusingthecachewrapperintheaboveexample.
5.DataControls
53
-
AssignIfNull
Ifyouhaveareasonforwantingtorecreatethegraphicpropertyforalistcell,youcanusetheassignIfNullhelper,whichwillassignavaluetoanygivenpropertyifthepropertydoesn'talreadycontainavalue.ThiswillmakesurethatyouavoidcreatingnewnodesifupdateItemiscalledonacellthatalreadyhasagraphicpropertyassigned.
cellFormat{graphicProperty().assignIfNull{label("Hello")}}
ListCellFragment
TheListCellFragmentisaspecialfragmentwhichcanhelpyoumanageListViewcells.ItextendsFragment,andincludessomeextraListViewspecificfieldsandhelpers.Youneverinstantiatethesefragmentsmanually,insteadyouinstructtheListViewtocreatethemasneeded.ThereisaonetoonecorrelationbetweenListCellandListCellFragmentinstances.OneListCellFragmentinstancewilloveritslifecyclebeusedtorepresentdifferentitems.
Tounderstandhowthisworks,let'sconsideramanuallyimplementedListCell,essentiallythewayyouwoulddoinvanillaJavaFX.TheupdateItemfunctionwillbecalledwhentheListCellshouldrepresentanewitem,noitem,orjustanupdatetothesameitem.WhenyouuseaListCellFragment,youdonotneedtoimplementsomethingakintoupdateItem,buttheitemPropertyinsideitwillupdatetorepresentthenewitemautomatically.YoucanlistentochangestotheitemProperty,orbetteryet,binditdirectlytoaViewModel.ThatwayyourUIcanbinddirectlytotheViewModelandnolongerneedtocareaboutchangestotheunderlyingitem.
Let'srecreatetheformfromthecellFormatexampleusingaListCellFragment.WeneedaViewModelwhichwewillcallPersonModel(PleaseseetheEditingModelsandValidationchapterforafullexplanationoftheViewModel)Fornow,justimaginethattheViewModelactsasaproxyforanunderlyingPerson,andthatthePersoncanbechangedwhiletheobservablevaluesintheViewModelremainthesame.WhenwehavecreatedourPersonCellFragment,weneedtoconfiguretheListViewtouseit:
listview(personlist){cellFragment(PersonCellFragment::class)}
5.DataControls
54
-
NowcomestheListCellFragmentitself.
classPersonListFragment:ListCellFragment(){valperson=PersonModel().bindTo(this)
overridevalroot=form{fieldset{field("Name"){label(person.name)}field("Birthday"){label(person.birthday)}label(stringBinding(person.age){"$valueyearsold"}){alignment=Pos.CENTER_RIGHTstyle{fontSize=22.pxfontWeight=FontWeight.BOLD}}}}}
BecausethisFragmentwillbereusedtorepresentdifferentlistitems,theeasiestapproachistobindtheuielementstotheViewModel'sproperties.
Thenameandbirthdaypropertiesarebounddirectlytothelabelsinsidethefields.TheagestringinthelastlabelneedstobeconstructedusingastringBindingtomakesureitupdateswhentheitemchanges.
WhilethismightseemlikeslightlymoreworkthanthecellFormatexample,thisapproachmakesitpossibletoleverageeverythingtheFragmentclasshastooffer.Italsoforcesyoutodefinethecellnodegraphoutsideofthebuilderhierarchy,whichimprovesrefactoringpossibilitiesandenablescodereuse.
Additionalhelpersandeditingsupport
TheListCellFragmentalsohavesomeotherhelperproperties.TheyincludethecellPropertywhichwillupdatewhenevertheunderlyingcellchangesandtheeditingProperty,whichwilltellyouifthistheunderlyinglistcellisineditingmode.TherearealsoeditinghelperfunctionscalledstartEdit,commitEdit,cancelEditplusanonEditcallback.TheListCellFragmentmakesittrivialtoutilizetheexistingeditingcapabilitesoftheListView.AcompleteexamplecanbeseenintheTodoMVCdemoapplication.
5.DataControls
55
https://github.com/edvin/todomvc
-
TableView
ProbablyoneofthemostsignificantbuildersinTornadoFXistheoneforTableView.IfyouhaveworkedwithJavaFX,youmighthaveexperiencedbuildingaTableViewinanobject-orientedway.ButTornadoFXprovidesafunctionaldeclarationconstructpatternusingextensionfunctionsthatgreatlysimplifiesthecodingofaTableView.
Sayyouhaveadomaintype,suchasPerson.
classPerson(valid:Int,valname:String,valbirthday:LocalDate){valage:Intget()=Period.between(birthday,LocalDate.now()).years}
TakeseveralinstancesofPersonandputtheminanObservableList.
privatevalpersons=listOf(Person(1,"SamanthaStuart",LocalDate.of(1981,12,4)),Person(2,"TomMarks",LocalDate.of(2001,1,23)),Person(3,"StuartGills",LocalDate.of(1989,5,23)),Person(3,"NicoleWilliams",LocalDate.of(1998,8,11))).observable()
YoucanquicklydeclareaTableViewwithallofitscolumnsusingafunctionalconstruct,andspecifytheitemspropertytoanObservableList(Figure5.3).
tableview(persons){column("ID",Person::id)column("Name",Person::name)column("Birthday",Person::birthday)column("Age",Person::age)}
Figure5.3
5.DataControls
56
-
Thecolumn()functionsareextensionfunctionsforTableViewacceptingaheadernameandamappedpropertyusingreflectionsyntax.TornadoFXwillthentakeeachmappingtorenderavalueforeachcellinthatgivencolumn.
IfyouwantgranularcontroloverTableViewcolumnresizepolicies,seeAppendixA2formoreinformationonSmartResizepolicies.
Using"Property"properties
IfyoufollowtheJavaFXPropertyconventionstosetupyourdomainclass,itwillautomaticallysupportvalueediting.
YoucancreatethesePropertyobjectstheconventionalway,oryoucanuseTornadoFX'spropertydelegatestoautomaticallycreatethesePropertydeclarationsasshownbelow.
classPerson(id:Int,name:String,birthday:LocalDate){varidbyproperty(id)funidProperty()=getProperty(Person::id)
varnamebyproperty(name)funnameProperty()=getProperty(Person::name)
varbirthdaybyproperty(birthday)funbirthdayProperty()=getProperty(Person::birthday)
valage:Intget()=Period.between(birthday,LocalDate.now()).years}
YouneedtocreatexxxProperty()functionsforeachpropertytosupportJavaFX'snamingconventionwhenitusesreflection.ThiscaneasilybedonebyrelayingtheircallstogetProperty()toretrievethePropertyforagivenfield.SeeAppendixA1fordetailed
5.DataControls
57
-
informationonhowthesepropertydelegateswork.
NowontheTableView,youcanmakeiteditable,maptotheproperties,andapplytheappropriatecell-editingfactoriestomakethevalueseditable.
overridevalroot=tableview(persons){isEditable=truecolumn("ID",Person::idProperty).useTextField(IntegerStringConverter())column("Name",Person::nameProperty).useTextField(DefaultStringConverter())column("Birthday",Person::birthdayProperty).useTextField(LocalDateStringConverter())column("Age",Person::age)}
Toalloweditingandrendering,TornadoFXprovidesafewdefaultcellfactoriesyoucaninvokeonacolumneasilythroughextensionfunctions.
ExtensionFunction Description
useTextField() UsesastandardTextFieldtoeditvalueswithaprovidedStringConverter
useComboBox() EditsacellvalueviaaComboBoxwithaspecifiedObservableListofapplicablevalues
useChoiceBox() AcceptsvaluechangestoacellwithaChoiceBox
useCheckBox() RendersaneditableCheckBoxforaBooleanvaluecolumn
useProgressBar() RendersthecellasaProgressBarforaDoublevaluecolumn
PropertySyntaxAlternatives
IfyoudonotcareaboutexposingthePropertyinafunction(whichiscommoninpractialusage)youcanexpressyourclasslikethis:
classPerson(id:Int,name:String,birthday:LocalDate){validProperty=SimpleIntegerProperty(id)varidbyidProperty
valnameProperty=SimpleStringProperty(name)varnamebynameProperty
valbirthdayProperty=SimpleObjectProperty(birthday)varbirthdaybybirthdayProperty
valage:Intget()=Period.between(birthday,LocalDate.now()).years}
5.DataControls
58
-
ThisalternativepatternexposesthePropertyasafieldmemberinsteadofafunction.Ifyouliketheabovesyntaxbutwanttokeepthefunction,youcanmakethepropertyprivateandaddthefunctionlikethis:
privatevalnameProperty=SimpleStringProperty(name)funnameProperty()=namePropertyvarnamebynameProperty
Choosingfromthesepatternsareallamatteroftaste,andyoucanusewhateverversionmeetsyourneedsorpreferencesbest.
YoucanalsoconvertplainpropertiestoJavaFXpropertiesusingtheTornadoFXPlugin.RefertoChapter13tolearnhowtodothis.
UsingcellFormat()
ThereareotherextensionfunctionsappliedtoTableViewthatcanassisttheflowofdeclaringaTableView.Forinstance,youcancallacellFormat()functiononagivencolumntoapplyformattingrules,suchashighlighting"Age"valueslessthan18(Figure5.4).
tableview(persons){column("ID",Person::id)column("Name",Person::name)column("Birthday",Person::birthday)column("Age",Person::age).cellFormat{text=it.toString()style{if(it<18){backgroundColor+=c("#8b0000")textFill=Color.WHITE}else{backgroundColor+=Color.WHITEtextFill=Color.BLACK}}}}
Figure5.4
5.DataControls
59
-
Accessingnestedproperties
Let'sassumeourPersonobjecthasaparentpropertywhichisalsoofoftypePerson.Tocreateacolumnfortheparentname,wehaveseveraloptions.Ourfirstattemptissimplyextractingthenamepropertymanually:
column("Parentname",{it.value.parentProperty.value.nameProperty})
Noticehowwecan'tsimplyreferencetheproperty,weneedtoaccessthevalueprovidedinthecallbacktogettotheactualinstanceandnestfromtheredowntothenameProperty.Whilethisworks,ithasonemajordrawback.Iftheparentchanges,thelistwon'tbeupdated.Wecanpartiallyremedythisbydefiningthevalueforthepropertyastheparentitself,andformattingit'sname:
column("Parentname",Person::parentProperty).cellFormat{textProperty().bind(it.parentProperty.value.nameProperty)}
Itmightstillnotupdaterightaway,eventhoughitwouldeventuallybecomeconsistentastheTableViewrefreshes.
Tocreateabindingthatwouldreflectachangetotheparentpropertyimmediately,considerusingaselectbinding:(moreonbindingslater)
column("Parentname",{it.value.parentProperty.select(Person::nameProperty)})
5.DataControls
60
-
DeclaringColumnValuesFunctionally
Ifyouneedtomapacolumn'svaluetoanon-property(suchasafunction),youcanuseanon-reflectionmeanstoextractthevaluesforthatcolumn.
SayyouhaveaWeeklyReporttypethathasagetTotal()functionacceptingaDayOfWeekargument(anenumofMonday,Tuesday...Sunday).
abstractclassWeeklyReport(valstartDate:LocalDate){abstractfungetTotal(dayOfWeek:DayOfWeek):BigDecimal}
Let'ssayyouwantedtocreateacolumnforeachDayOfWeek.Youcannotmaptoproperties,butyoucanmapeachWeeklyReportitemexplicitlytoextracteachvalueforthatDayOfWeek.
tableview{for(dayOfWeekinDayOfWeek.values()){column(dayOfWeek.toString()){ReadOnlyObjectWrapper(it.value.getTotal(dayOfWeek))}}}
ThismorecloselyresemblesthetraditionalsetCellValueFactory()fortheJavaFXTableColumn.
RowExpanders
LaterwewilllearnabouttheTreeTableViewwhichhasanotionof"parent"and"child"rows,buttheconstraintwiththiscontrolistheparentandchildmusthavethesamecolumns.Fortunately,TornadoFXcomeswithanawesomeutilitytonotonlyreveala"childtable"foragivenrow,butanykindofNodecontrol.
Saywehavetwodomaintypes:RegionandBranch.ARegionisageographicalzone,anditcontainsoneormoreBranchitemswhicharespecificbusinessoperationlocations(warehouses,distributioncenters,etc).Hereisadeclarationofthesetypesandsomegiveninstances.
5.DataControls
61
-
classRegion(valid:Int,valname:String,valcountry:String,valbranches:ObservableList)
classBranch(valid:Int,valfacilityCode:String,valcity:String,valstateProvince:String)
valregions=listOf(Region(1,"PacificNorthwest","USA",listOf(Branch(1,"D","Seattle","WA"),Branch(2,"W","Portland","OR")).observable()),Region(2,"Alberta","Canada",listOf(Branch(3,"W","Calgary","AB")).observable()),Region(3,"Midwest","USA",listOf(Branch(4,"D","Chicago","IL"),Branch(5,"D","Frankfort","KY"),Branch(6,"W","Indianapolis","IN")).observable())).observable()
WecancreateaTableViewwhereeachrowhasarowExpander()functiondefined,andtherewecanarbitrarilycreateanyNodecontrolbuiltoffthatparticularrow'sitem.Inthiscase,wecannestanotherTableViewforagivenRegiontoshowalltheBranchitemsbelongingtoit.Itwillhavea"+"buttoncolumntoexpandandshowthisexpandedcontrol(Figure5.5).
Figure5.5
5.DataControls
62
-
Thereareafewconfigurabilityoptions,like"expandondouble-click"behaviorsandaccessingtheexpanderColumn(thecolumnwiththe"+"button)todriveapadding(Figure5.6).
overridevalroot=tableview(regions){column("ID",Region::id)column("Name",Region::name)column("Country",Region::country)rowExpander(expandOnDoubleClick=true){paddingLeft=expanderColumn.widthtableview(it.branches){column("ID",Branch::id)column("FacilityCode",Branch::facilityCode)column("City",Branch::city)column("State/Province",Branch::stateProvince)}}}
Figure5.6
5.DataControls
63
-
TherowExpander()functiondoesnothavetoreturnaTableViewbutanykindofNode,includingFormsandothersimpleorcomplexcontrols.
Accessingtheexpandercolumn
Youmightwanttomanipulateorcallfunctionsontheactualexpandercolumn.Ifyouactivateexpandondoubleclick,youmightnotwanttoshowtheexpandercolumninthetableatall.Firstweneedareferencetotheexpander:
valexpander=rowExpander(true){...}
Ifyouwanttohidetheexpandercolumn,justcallexpander.isVisible=false.Youcanalsoprogrammaticallytoggletheexpandedstateofanycolumnbycallingexpander.toggleExpanded(rowIndex).
TreeView
TheTreeViewcontainselementswhereeachelementmaycontainchildelements.Typicallyarrowsallowyoutoexpandaparentelementtoseeitschildren.Forinstance,wecannestemployeesunderdepartmentnames
5.DataControls
64
-
TraditionallyinJavaFX,populatingtheseelementsisrathercumbersomeandverbose.FortunatelyTornadoFXmakesitrelativelysimple.
SayyouhaveasimpletypePersonandanObservableListcontainingseveralinstances.
dataclassPerson(valname:String,valdepartment:String)
valpersons=listOf(Person("MaryHanes","Marketing"),Person("SteveFolley","CustomerService"),Person("JohnRamsy","ITHelpDesk"),Person("ErlickFoyes","CustomerService"),Person("ErinJames","Marketing"),Person("JacobMays","ITHelpDesk"),Person("LarryCable","CustomerService"))
CreatingaTreeViewwiththetreeview()buildercanbedonefunctionallyFigure5.7).
//CreatePersonobjectsforthedepartments//withthedepartmentnameasPerson.name
valdepartments=persons.map{it.department}.distinct().map{Person(it,"")}
treeview{//Createrootitemroot=TreeItem(Person("Departments",""))
//MakesurethetextineachTreeItemisthenameofthePersoncellFormat{text=it.name}
//Generateitems.Childrenoftherootitemwillcontaindepartmentspopulate{parent->if(parent==root)departmentselsepersons.filter{it.department==parent.value.name}}}
Figure5.7
5.DataControls
65
-
Let'sbreakthisdown:
valdepartments=persons.map{it.department}.distinct().map{Person(it,"")}
Firstwegatheradistinctlistofallthedepartmentsderivedfromthepersonslist.ButthenweputeachdepartmentStringinaPersonobjectsincetheTreeViewonlyacceptsPersonelements.Whilethisisnotveryintuitive,thisistheconstraintanddesignofTreeView.WemustmakeeachdepartmentaPersonforittobeaccepted.
treeview{//Createrootitemroot=TreeItem(Person("Departments",""))
5.DataControls
66
-
NextwespecifythehighestrootfortheTreeViewthatalldepartmentswillbenestedunder,andwegiveitaplaceholderPersoncalled"Departments".
cellFormat{text=it.name}
ThenwespecifythecellFormat()torenderthenameofeachPerson(includingdepartments)oneachcell.
populate{parent->if(parent==root)departmentselsepersons.filter{it.department==parent.value.name}}
Finally,wecallthepopulate()functionandprovideablockinstructinghowtoprovidechildrentoeachparent.Iftheparentisindeedtheroot,thenwereturnthedepartments.OtherwisetheparentisadepartmentandweprovidealistofPersonobjectsbelongingtothatdepartment.
DatadrivenTreeView
IfthechildlistyoureturnfrompopulateisanObservableList,anychangestothatlistwillautomaticallybereflectedintheTreeView.Thepopulatefunctionwillbecalledforanynewchildrenthatappears,andremoveditemswillresultinremovedTreeItemsaswell.
TreeViewwithDifferingTypes
ItisnotnecessarilyintuitivetomakeeveryentityinthepreviousexampleaPerson.WemadeeachdepartmentaPersonaswellastheroot"Departments".ForamorecomplexTreeViewwhereTisunknownandcanbeanynumberoftypes,itisbettertoleveragestarprojectionfortypeT.
Usingstarprojection,youcansafelypopulatemultipletypesnestedintotheTreeView.
Forinstance,youcancreateaDepartmenttypeandleveragecellFormat()toutilizetype-checkingforrendering.Thenyoucanuseapopulate()functionthatwilliterateovereachelement,andyouspecifythechildrenforeachelement(ifany).
5.DataControls
67
http://kotlinlang.org/docs/reference/generics.html#star-projections
-
dataclassDepartment(valname:String)
//CreateDepartmentobjectsforthedepartmentsbygettingdistinctvaluesfromPerson.departmentvaldepartments=persons.map{it.department}.distinct().map{Department(it)}
//TypesafewayofextractingthecorrectTreeItemtextcellFormat{text=when(it){isString->itisDepartment->it.nameisPerson->it.nameelse->throwIllegalArgumentException("Invalidvaluetype")}}
//Generateitems.Childrenoftherootitemwillcontaindepartments,childrenofdepartmentsarefilteredpopulate{parent->valvalue=parent.valueif(parent==root)departmentselseif(valueisDepartment)persons.filter{it.department==value.name}elsenull}
TreeTableView
TheTreeTableViewoperatesandfunctionssimilarlytoaTreeView,butithasmultiplecolumnssinceitisatable.PleasenotethatthecolumnsinaTreeTableViewarethesameforeachparentandchildelement.Ifyouwantthecolumnstobedifferentbetweenparentandchild,useaTableViewwitharowExpander()ascoveredearlierinthischapter.
SayyouhaveaPersonclassthatoptionallyhasanemployeesparameter,whichdefaultstoanemptyListifnobodyreportstothatPerson.
classPerson(valname:String,valdepartment:String,valemail:String,valemployees:List=emptyList())
ThenyouhaveanObservableListholdinginstancesofthisclass.
5.DataControls
68
-
valpersons=listOf(Person("MaryHanes","ITAdministration","[email protected]",listOf(Person("JacobMays","ITHelpDesk","[email protected]"),Person("JohnRamsy","ITHelpDesk","[email protected]"))),Person("ErinJames","HumanResources","[email protected]",listOf(Person("ErlickFoyes","CustomerService","[email protected]"),Person("SteveFolley","CustomerService","[email protected]"),Person("LarryCable","CustomerService","[email protected]")))).observable()
YoucancreateaTreeTableViewbymergingthecomponentsneededforaTableViewandTreeViewtogether.Youwillneedtocallthepopulate()functionaswellassettherootTreeItem.
valtreeTableView=TreeTableView().apply{column("Name",Person::nameProperty)column("Department",Person::departmentProperty)column("Email",Person::emailProperty)
///Createtherootitemthatholdsalltoplevelemployeesroot=TreeItem(Person("Employeesbyleader","","",persons))
//Alwaysreturnemployeesunderthecurrentpersonpopulate{it.value.employees}
//Expandthetwofirstlevelsroot.isExpanded=trueroot.children.forEach{it.isExpanded=true}
//ResizetodisplayallelementsonthefirsttwolevelsresizeColumnsToFitContent()}
ItisalsopossibletoworkwithmoreofanadhocbackingstorelikeaMap.Thatwouldlooksomethinglikethis:
5.DataControls
69
-
valtableData=mapOf("Fruit"toarrayOf("apple","pear","Banana"),"Veggies"toarrayOf("beans","cauliflower","cale"),"Meat"toarrayOf("poultry","pork","beef"))
treetableview(TreeItem("Items")){column("Type",{it.value.valueProperty()})populate{if(it.value=="Items")tableData.keyselsetableData[it.value]?.asList()}}
DataGrid
ADataGridissimilartotheGridPaneinthatitdisplaysitemsinaflexiblegridofrowsandcolumns,butthesimilaritiesendsthere.WhiletheGridPanerequiresyoutoaddNodestothechildrenlist,theDataGridisdatadriveninthesamewayasTableViewandListView.Yousupplyitwithalistofitemsandtellithowtoconvertthosechildrentoagraphicalrepresentation.
Itsupportsselectionofeitherasingleitemormultipleitemsatatimesoitcanbeusedasforexamplethedisplayofanimageviewerorothercomponentswhereyouwantavisualrepresentationoftheunderlyingdata.UsagewiseitisclosetoaListView,butyoucancreateanarbitraryscenegraphinsideeachcellsoitiseasytovisualizemultiplepropertiesforeachitem.
valkittens=listOf("http://i.imgur.com/DuFZ6PQb.jpg","http://i.imgur.com/o2QoeNnb.jpg")//moreitemshere
datagrid(kittens){cellCache{imageview(it)}}
Figure5.8
5.DataControls
70
-
ThecellCachefunctionreceiveseachiteminthelist,andsinceweusedalistofStringsinourexample,wesimplypassthatstringtotheimageview()buildertocreateanImageViewinsideeachtablecell.ItisimportanttocallthecellCachefunctioninsteadofthecellFormatfunctiontoavoidrecreatingtheimageseverytimetheDataGridredraws.Itwillreusetheitems.
Let'screateascenegraphthatisalittlebitmoreinvolved,andalsochangethedefaultsizeofeachcell:
5.DataControls
71
-
valnumbers=(1..10).toList()
datagrid(numbers){cellHeight=75.0cellWidth=75.0
multiSelect=true
cellCache{stackpane{circle(radius=25.0){fill=Color.FORESTGREEN}label(it.toString())}}}
Figure5.9
Thegridissuppliedwithalistofnumbersthistime.Westartbyspecifyingacellheightandwidthof75pixels,halfofthedefaultsize.Wealsoconfiguremultiselecttobeabletoselectmorethanasingleelement.ThisisashortcutofwritingselectionModel.selectionMode=SelectionMode.MULTIPLEviaanextensionproperty.WecreateaStackPanethatstacksaLabelontopofaCircle.
5.DataControls
72
-
Youmightwonderwhythelabelgotsobigandboldbydefault.Thisiscomingfromthedefaultstylesheet.Thestylesheetisagoodstartingpointforfurthercustomization.AllpropertiesofthedatagridcanbeconfiguredincodeaswellasinCSS,andthestylesheetlistsallpossiblestyleproperties.
Thenumberlistshowcasedmultipleselection.Whenacellisselected,itreceivestheCSSpseudoclassofselected.BydefaultitwillbehavemostlylikeaListViewrowwithregardstoselectionstyles.YoucanaccesstheselectionModelofthedatagridtolistenforselectionchanges,seewhatitemsareselectedetc.
Summary
FunctionalconstructsworkwellwithdatacontrolslikeTableView,TreeView,andotherswehaveseeninthischapter.Usingthebuilderpatterns,youcanquicklyandfunctionallydeclarehowdataisdisplayed.
InChapter7,wewillembedcontrolsinlayoutstocreatemorecomplexUI'seasily.
5.DataControls
73
https://github.com/edvin/tornadofx/blob/master/src/main/resources/tornadofx/datagrid.css
-
Type-SafeCSSWhileyoucancreateplaintextCSSstylesheetsinJavaFX,TornadoFXprovidestheoptiontobringtype-safetyandcompiledCSStoJavaFX.Youcanconvenientlychoosetocreatestylesinitsownclass,ordoitinlinewithinacontroldeclaration.
InlineCSS
ThequickestandeasiestwaytostyleacontrolontheflyistocallagivenNode'sinlinestyle{}function.AlltheCSSpropertiesavailableonagivencontrolareavailableinatype-safemanner,withcompilationchecksandauto-completion.
Forexample,youcanstylethebordersonaButton(usingthebox()function),bolditsfont,androtateit(Figure6.1).
button("PressMe"){style{fontWeight=FontWeight.EXTRA_BOLDborderColor+=box(top=Color.RED,right=Color.DARKGREEN,left=Color.ORANGE,bottom=Color.PURPLE)rotate=45.deg}
setOnAction{println("Youpressedthebutton")}}
Figure6.1
ThisisespeciallyhelpfulwhenyouwanttostyleacontrolwithoutbreakingthedeclarationflowoftheButton.However,keepinmindthestyle{}willreplaceallstylesappliedtothatcontrolunlessyoupasstrueforitsoptionalappendargument.
6.TypeSafeCSS
74
-
style(append=true){....}
Sometimesyouwanttoapplythesamestylestomanynodesinonego.Thestyle{}functioncanalsobeappliedtoanyIterablethatcontainsNodes:
vbox{label("First")label("Second")label("Third")children.style{fontWeight=FontWeight.BOLD}}
ThefontWeightstyleisappliedtoallchildrenofthevbox,inessenceallthelabelsweadded.
Whenyourstylingcomplexitypassesacertainthreshold,youmaywanttoconsiderusingStylesheetswhichwewillcovernext.
ApplyingStyleClasseswithStylesheets
Ifyouwanttoorganize,re-use,combine,andoverridestylesyouneedtoleverageaStylesheet.TraditionallyinJavaFX,astylesheetisdefinedinaplainCSStextfileincludedintheproject.However,TornadoFXallowscreatingstylesheetswithpureKotlincode.Thishasthebenefitsofcompilationchecks,auto-completion,andotherperksthatcomewithstaticallytypedcode.
TodeclareaStylesheet,extenditontoyourownclasstoholdyourcustomizedstyles.
importtornadofx.*
classMyStyle:Stylesheet(){}
Next,youwillwanttospecifyitscompanionobjecttoholdclass-levelpropertiesthatcaneasilyberetrieved.Declareanewcssclass()-delegatedpropertycalledtackyButton,anddefinefourcolorswewilluseforitsborders.
6.TypeSafeCSS
75
-
importjavafx.scene.paint.Colorimporttornadofx.*
classMyStyle:Stylesheet(){
companionobject{valtackyButtonbycssclass()
privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}}
Notealsoyoucanusethec()functiontobuildcolorsquicklyusingRGBvaluesorcolorStrings.
privatevaltopColor=c("#FF0000")privatevalrightColor=c("#006400")privatevalleftColor=c("#FFA500")privatevalbottomColor=c("#800080")
Finally,declareaninit()blocktoapplystylingtotheclasses.Defineyourselectionandprovideablockthatmanipulatesitsvariousproperties.(Forcompoundselections,callthes()function,whichisanaliasfortheselect()function).Setrotateto10degrees,definetheborderColorusingthefourcolorsandthebox()function,makethefontfamily"ComicSansMS",andincreasethefontSizeto20pixels.NotethatthereareextensionpropertiesforNumbertypestoquicklyyieldthevalueinthatunit,suchas10.degfor10degreesand20.pxfor20pixels.
6.TypeSafeCSS
76
-
importjavafx.scene.paint.Colorimporttornadofx.*
classMyStyle:Stylesheet(){
companionobject{valtackyButtonbycssclass()
privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}
init{tackyButton{rotate=10.degborderColor+=box(topColor,rightColor,bottomColor,leftColor)fontFamily="ComicSansMS"fontSize=20.px}}}
NowyoucanapplythetackyButtonstyletobuttons,labels,andothercontrolsthatsupporttheseproperties.Whilethisstylingcanworkwithothercontrolslikelabels,wearegoingtotargetbuttonsinthisexample.
First,loadtheMyStylestylesheetintoyourapplicationbyincludingitascontructorparameter.
classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}
ThereloadStylesheetsOnFocus()functioncallwillinstructTornadoFXtoreloadtheStylesheetseverytimetheStagegetsfocus.Youcanalsopassthe--live-stylesheetsargumenttotheapplicationtoaccomplishthis.
Important:Forthereloadtowork,youmustberunningtheJVMindebugmodeandyoumustinstructyourIDEtorecompilebeforeyouswitchbacktoyourapp.Withoutthesesteps,nothingwillhappen.ThisalsoappliestoreloadViewsOnFocus()whichissimilar,butreloadsthewholeviewinsteadofjustthestylesheet.Thisway,youcanevolveyourUIveryrapidlyina"codechange,compile,refresh"manner.
6.TypeSafeCSS
77
-
YoucanapplystylesdirectlytoacontrolbycallingitsaddClass()function.ProvidetheMyStyle.tackyButtonstyletotwobuttons(Figure6.2).
classMyView:View(){overridevalroot=vbox{button("PressMe"){addClass(MyStyle.tackyButton)}button("PressMeToo"){addClass(MyStyle.tackyButton)}}}
Figure6.2
IntellijIDEAcanperformaquickfixtoimportmembervariables,allowingaddClass(MyStyle.tackyButton)tobeshortenedtoaddClass(tackyButton)ifyouprefer.
YoucanuseremoveClass()toremovethespecifiedstyleaswell.
TargetingStylestoaType
OneofthebenefitsofusingpureKotlinisyoucantightlymanipulateUIcontrolbehaviorandconditionsusingKotlincode.Forexample,youcanapplythestyletoanyButtonbyiteratingthroughacontrol'schildren,filteringforonlychildrenthatareButtons,andapplyingtheaddClass()tothem.
classMyView:View(){overridevalroot=vbox{button("PressMe")button("PressMeToo")
children.asSequence().filter{itisButton}.forEach{it.addClass(MyStyle.tackyButton)}}}
6.TypeSafeCSS
78
-
Infact,manipulatingclassesonseveralnodesatonceissocommonthatTornadoFXprovidesashortcutforit:
children.filter{itisButton}.addClass(MyStyle.tackyButton)}
YoucanalsotargetallButtoninstancesinyourapplicationbyselectingandmodifyingthebuttonintheStylesheet.ThiswillapplythestyletoallButtons.
importjavafx.scene.paint.Colorimporttornadofx.*
classMyStyle:Stylesheet(){
companionobject{valtackyButtonbycssclass()
privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}
init{button{rotate=10.degborderColor+=box(topColor,rightColor,leftColor,bottomColor)fontFamily="ComicSansMS"fontSize=20.px}}}
importjavafx.scene.layout.VBoximporttornadofx.*
classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}
classMyView:View(){overridevalroot=vbox{button("PressMe")button("PressMeToo")}}
6.TypeSafeCSS
79
-
Figure6.3
Notealsoyoucanselectmultipleclassesandcontroltypestomix-and-matchstyles.Forexample,youcansetthefontsizeoflabelsandbuttonsto20pixels,andcreatetackybordersandfontsonlyforbuttons(Figure6.4).
classMyStyle:Stylesheet(){
companionobject{
privatevaltopColor=Color.REDprivatevalrightColor=Color.DARKGREENprivatevalleftColor=Color.ORANGEprivatevalbottomColor=Color.PURPLE}
init{s(button,label){fontSize=20.px}button{rotate=10.degborderColor+=box(topColor,rightColor,leftColor,bottomColor)fontFamily="ComicSansMS"}}}
classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}
classMyView:View(){overridevalroot=vbox{label("LoremIpsum")button("PressMe")button("PressMeToo")}}
6.TypeSafeCSS
80
-
Figure6.4
Multi-ValueCSSProperties
SomeCSSpropertiesacceptmultiplevalues,andTornadoFXStylesheetscanstreamlinethiswiththemulti()function.ThisallowsyoutospecifymultiplevaluesviaavarargsparameterandletTornadoFXtakecareoftherest.Forinstance,youcannestmultiplebackgroundcolorsandinsetsintoacontrol(Figure6.5).
label("LoreIpsum"){style{fontSize=30.pxbackgroundColor=multi(Color.RED,Color.BLUE,Color.YELLOW)backgroundInsets=multi(box(4.px),box(8.px),box(12.px))}}
Figure6.5
Themulti()functionshouldworkwherevermultiplevaluesareaccepted.Ifyouwanttoonlyassignasinglevaluetoapropertythatacceptsmultiplevalues,youwillneedtousetheplusAssign()operatortoaddit(Figure6.6).
6.TypeSafeCSS
81
-
label("LoreIpsum"){style{fontSize=30.pxbackgroundColor+=Color.REDbackgroundInsets+=box(4.px)}}
Figure6.6
NestingStyles
Insideaselectorblockyoucanapplyfurtherstylestargetingchildcontrols.
Forinstance,defineaCSSclasscalledcritical.Makeitputanorangeborderaroundanycontrolitisappliedto,andpaditby5pixels.
classMyStyle:Stylesheet(){
companionobject{valcriticalbycssclass()}
init{critical{borderColor+=box(Color.ORANGE)padding=box(5.px)}}}
Butsupposewhenweappliedcriticaltoanycontrol,suchasanHBox,wewantittoaddadditionalstylingstobuttonsinsidethatcontrol.Nestinganotherselectionwilldothetrick.
6.TypeSafeCSS
82
-
classMyStyle:Stylesheet(){companionobject{valcriticalbycssclass()}init{critical{borderColor+=box(Color.ORANGE)padding=box(5.px)button{backgroundColor+=Color.REDtextFill=Color.WHITE}}}}
Nowwhenyouapplycriticaltosay,anHBox,allbuttonsinsidethatHBoxwillgetthatdefinedstyleforbutton(Figure6.7)
classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}
classMyView:View(){overridevalroot=hbox{addClass(MyStyle.critical)button("Warning!")button("Danger!")}}
Figure6.7
Thereisonecriticalthingtonotconfusehere.TheorangeborderisonlyappliedtotheHBoxsincethecriticalclasswasappliedtoit.ThebuttonsdonotgetanorangeborderbecausetheyarechildrentotheHBox.Whiletheirstyleisdefinedbycritical,theydonotinheritthestylesoftheirparent,onlythosedefinedforbutton.
6.TypeSafeCSS
83
-
Ifyouwantthebuttonstogetanorangebordertoo,youneedtoapplythecriticalclassdirectlytothem.Youwillwanttousetheand()toapplyspecificstylestobuttonsthatarealsodeclaredascritical.
classMyStyle:Stylesheet(){
companionobject{valcriticalbycssclass()}
init{critical{
borderColor+=box(Color.ORANGE)padding=box(5.px)
and(button){backgroundColor+=Color.REDtextFill=Color.WHITE}}}}
classMyApp:App(MyView::class,MyStyle::class){init{reloadStylesheetsOnFocus()}}
classMyView:View(){overridevalroot=hbox{addClass(MyStyle.critical)
button("Warning!"){addClass(MyStyle.critical)}
button("Danger!"){addClass(MyStyle.critical)}}}
Figure6.8
6.TypeSafeCSS
84
-
NowyouhaveorangebordersaroundtheHBoxaswellasthebuttons.Whennestingstyles,keepinmindthatwrappingtheselectionwithand()willcascadestylestochildrencontrolsorclasses.
MixinsTherearetimesyoumaywanttoreuseasetofstylingsandapplythemtoseveralcontrolsandselectors.Thispreventsyoufromhavingtoredundantlydefinethesamepropertiesandvalues.Forinstance,ifyouwanttocreateasetofstylingcalledredAllTheThings,youcoulddefineitasamixinasshownbelow.ThenyoucanreuseitforaredStyleclass,aswellasatextInput,alabel,andapasswordFieldwithadditionalstylemodifications(Figure6.9).
Stylesheet
6.TypeSafeCSS
85
-
importjavafx.scene.paint.Colorimportjavafx.scene.text.FontWeightimporttornadofx.*
classStyles:Stylesheet(){
companionobject{valredStylebycssclass().}
init{valredAllTheThings=mixin{backgroundInsets+=box(5.px)borderColor+=box(Color.RED)textFill=Color.RED}
redStyle{+redAllTheThings}
s(textInput,label){+redAllTheThingsfontWeight=FontWeight.BOLD}
passwordField{+redAllTheThingsbackgroundColor+=Color.YELLOW}}}
AppandView
6.TypeSafeCSS
86
-
classMyApp:App(MyView::class,Styles::class)
classMyView:View("MyView"){overridevalroot=vbox{label("Enteryourlogin")form{fieldset{field("Username"){textfield()}field("Password"){passwordfield()}}}button("Go!"){addClass(Styles.redStyle)}}}
Figure6.9
ThestylesheetisappliedtotheapplicationbyaddingitasaconstructorparametertotheAppclass.Thisisavarargparameter,soyoucansendinacommaseparatedlistofmultiplestylesheets.Ifyouwanttoloadstylesheetsdynamicallybasedonsomecondition,youcancallimportStylesheet(Styles::classfromanywhere.AnyUIComponentopenedafterthecalltoimportStylesheetwillgetthestylesheetapplied.Youcanalsoloadnormaltextbasedcssstylesheetswiththisfunction:
importStylesheet("/mystyles.css")
Loadingatextbasedcssstylesheet
IfyoufindyouarerepeatingyourselfsettingthesameCSSpropertiestothesamevalues,youmightwanttoconsiderusingmixinsandreusingthemwherevertheyareneededinaStylesheet.
6.TypeSafeCSS
87
-
ModifierSelectionsTornadoFXalsosupportsmodifierselectionsbyleveragingand()functionswithinaselection.Themostcommoncasethisishandyisstylingfor"selected"andcursor"hover"contextsforacontrol.
IfyouwantedtocreateaUIthatwillmakeanyButtonredwhenitishoveredover,andanyselectedCellindatacontrolssuchasListViewred,youcandefineaStylesheetlikethis(Figure6.10).
Stylesheet
importjavafx.scene.paint.Colorimporttornadofx.Stylesheet
classStyles:Stylesheet(){
init{button{and(hover){backgroundColor+=Color.RED}}cell{and(selected){backgroundColor+=Color.RED}}}}
AppandView
importtornadofx.*
classMyApp:App(MyView::class,Styles::class)
classMyView:View("MyView"){
vallistItems=listOf("Alpha","Beta","Gamma").observable()andoverridevalroot=vbox{button("Hoveroverme")listview(listItems)}}
6.TypeSafeCSS
88
-
Figure6.10-AcellisselectedandtheButtonisbeinghoveredover.Botharenowred.
Wheneveryouneedmodifiers,usetheselect()functiontomakethosecontextualstylemodifications.
Control-SpecificStylesheetsIfyoudecidetocreateyourowncontrols(oftenbyextendinganexistingcontrol,likeButton),JavaFXallowsyoutopairastylesheetwithit.Inthissituation,itisadvantageoustoloadthisStylesheetonlywhenthiscontrolisloaded.Forinstance,ifyouhaveaDangerButtonclassthatextendsButton,youmightconsidercreatingaStylesheetspecificallyforthatDangerButton.ToallowJavaFXtoloadit,youneedtooverridethegetUserAgentStyleSheet()functionasshownbelow.Thiswillconvertyourtype-safeStylesheetintoplaintextCSSthatJavaFXnativelyunderstands.
classDangerButton:Button("Danger!"){init{addClass(DangerButtonStyles.dangerButton)}overridefungetUserAgentStylesheet()=DangerButtonStyles().base64URL.toExternalForm()}
classDangerButtonStyles:Stylesheet(){companionobject{valdangerButtonbycssclass()}
init{dangerButton{backgroundInsets+=box(0.px)fontWeight=FontWeight.BOLDfontSize=20.pxpadding=box(10.px)}}}
6.TypeSafeCSS
89
-
TheDangerButtonStyles().base64URL.toExternalForm()expressioncreatesaninstanceoftheDangerButtonStyles,andturnsitintoaURLcontainingtheentirestylesheetthatJavaFXcanconsume.
ConclusionTornadoFXdoesagreatjobexecutingabrilliantconcepttomakeCSStype-safe,anditfurtherdemonstratesthepowerofKotlinDSL's.Configurationthroughstatictextfilesisslowtoexpresswith,buttype-safeCSSmakesitfluentandquickespeciallywithIDEauto-completion.EvenifyouarepragmaticaboutUI'sandfeelstylingissuperfluous,therewillbetimesyouneedtoleverageconditionalformattingandhighlightingsorules"popout"inaUI.Atminimum,getcomfortableusingtheinlinestyle{}blocksoyoucanquicklyaccessstylingpropertiesthatcannotbeaccessedanyotherway(suchasTextWeight).
6.TypeSafeCSS
90
-
LayoutsandMenusComplexUI'srequiremanycontrols.Itislikelythesecontrolsneedtobegrouped,positioned,andsizedwithsetpolicies.FortunatelyTornadoFXstreamlinesmanylayoutsthatcomewithJavaFX,aswellasfeaturesitsownproprietaryFormlayout.
TornadoFXalsohastype-safebuilderstocreatemenusinahighlystructured,declarativeway.MenuscanbeespeciallycumbersometobuildusingconventionalJavaFXcode,andKotlinreallyshinesinthisdepartment.
BuildersforLayoutsLayoutsgroupcontrolsandsetpoliciesabouttheirsizingandpositioningbehavior.Technically,layoutsthemselvesarecontrolssothereforeyoucannestlayoutsinsidelayouts.ThisiscriticalforbuildingcomplexUI's,andTornadoFXmakesmaintenanceofUIcodeeasierbyvisiblyshowingthenestedrelationships.
VBox
AVBoxstackscontrolsverticallyintheordertheyaredeclaredinsideitsblock(Figure7.1).
vbox{button("Button1").setOnAction{println("Button1Pressed")}button("Button2").setOnAction{println("Button2Pressed")}}
Figure7.1
YoucanalsocallvboxConstraints()withinachild'sblocktochangethemarginandverticalgrowingbehaviorsoftheVBox.
7.LayoutsandMenus
91
-
vbox{button("Button1"){vboxConstraints{marginBottom=20.0vGrow=Priority.ALWAYS}}button("Button2")}
YoucanuseashorthandextensionpropertyforvGrowwithoutcallingvboxConstraints().
vbox{button("Button1"){vGrow=Priority.ALWAYS}button("Button2")}
HBox
HBoxbehavesalmostidenticallytoVBox,butitstacksallcontrolshorizontallyleft-to-rightintheorderdeclaredinitsblock.
hbox{button("Button1").setOnAction{println("Button1Pressed")}button("Button2").setOnAction{println("Button2Pressed")}}
Figure7.2
Youcanalsocallhboxconstraints()withintheachild'sblocktochangethemarginandhorizontalgrowingbehaviorsoftheHBox.
7.LayoutsandMenus
92
-
hbox{button("Button1"){hboxConstraints{marginRight=20.0hGrow=Priority.ALWAYS}}button("Button2")}
YoucanuseashorthandextensionpropertyforhGrowwithoutcallinghboxConstraints().
hbox{button("Button1"){hGrow=Priority.ALWAYS}button("Button2")}
FlowPane
TheFlowPanelaysoutcontrolsleft-to-rightandwrapstothenextlineontheboundary.Forexample,sayyouadded100buttonstoaFlowPane(Figure7.3).Youwillnoticeitsimplylaysoutbuttonsfromleft-to-right,andwhenitrunsoutofroomitmovestothe"nextline".
flowpane{for(iin1..100){button(i.toString()){setOnAction{println("Youpressedbutton$i")}}}}
Figure7.3
7.LayoutsandMenus
93