TornadoFX編程指南省核,第3章,組件

譯自《Components》

組件

JavaFX使用戲劇類比來(lái)組織一個(gè)包含StageScene組件的Application昆码。 TornadoFX通過(guò)提供View气忠,ControllerFragment組件也構(gòu)建在此類比基礎(chǔ)之上。 雖然TornadoFX也使用StageScene赋咽,但View旧噪,ControllerFragment引入了可以簡(jiǎn)化開發(fā)的新概念。 這些組件多數(shù)被自動(dòng)維護(hù)為單例(singletons)脓匿,并且可以通過(guò)簡(jiǎn)單的依賴注入(dependency injections)和其他方式相互通信淘钟。

您還可以選擇使用FXML,稍后會(huì)討論陪毡。 但首先米母,讓我們繼承App來(lái)創(chuàng)建用于啟動(dòng)TornadoFX應(yīng)用程序的入口點(diǎn)。

App和View的基礎(chǔ)知識(shí)

要?jiǎng)?chuàng)建TornadoFX應(yīng)用程序毡琉,您必須至少有一個(gè)繼承了App的類铁瞒。App是應(yīng)用程序的入口點(diǎn),并指定初始View桅滋。 實(shí)際上它繼承了JavaFX的Application 精拟,但是您不一定需要指定一個(gè)start()main()方法。

但首先虱歪,讓我們繼承App來(lái)創(chuàng)建自己的實(shí)現(xiàn)蜂绎,并將主視圖(primary view)指定為構(gòu)造函數(shù)的第一個(gè)參數(shù)。

class MyApp: App(MyView::class)

視圖(View)包含顯示邏輯以及節(jié)點(diǎn)(Nodes)的布局笋鄙,類似于JavaFX的Stage师枣。 它被作為單例(singleton)來(lái)自動(dòng)管理。 當(dāng)您聲明一個(gè)View萧落,您必須指定一個(gè)root屬性践美,該屬性可以是任何Node類型,并且將保存視圖(View)的內(nèi)容找岖。

在同一個(gè)Kotlin文件或在另一個(gè)新文件中陨倡,從View繼承出來(lái)一個(gè)新類。 覆蓋其抽象root屬性并賦值VBox许布,或您選擇的任何其他Node兴革。

class MyView: View() {
    override val root = VBox()
}

但是,我們可能想填充這個(gè)VBox,作為root控件杂曲。 使用初始化程序塊 (initializer block)庶艾,讓我們添加一個(gè)JavaFX的Button和一個(gè)Label。 您可以使用 “plus assign” +=運(yùn)算符將子項(xiàng)添加到任何Pane類型擎勘,包括這里的VBox咱揍。

class MyView: View() {
    override val root = VBox()

    init {
        root += Button("Press Me")
        root += Label("")
    }
}

雖然從查看上述代碼來(lái)看,很清楚發(fā)生了什么棚饵,但TornadoFX還提供了一個(gè)構(gòu)建器語(yǔ)法(builder syntax)煤裙,可以進(jìn)一步簡(jiǎn)化您的UI代碼,并可通過(guò)查看代碼來(lái)更輕松地推導(dǎo)出最終的UI噪漾。 我們將逐漸轉(zhuǎn)向構(gòu)建器語(yǔ)法硼砰,最后在下一章中全面介紹構(gòu)建器(builders)。

雖然我們會(huì)向您介紹新概念怪与,但您可能有時(shí)還會(huì)看到?jīng)]有使用最佳做法的代碼。 我們這樣做是為了向您介紹這些概念缅疟,并讓您更深入地了解底層發(fā)生的情況分别。 逐漸地,我們將會(huì)以更好的方式介紹更強(qiáng)大的結(jié)構(gòu)來(lái)解決這個(gè)問(wèn)題存淫。

接下來(lái)我們將看到如何運(yùn)行這個(gè)應(yīng)用程序耘斩。

啟動(dòng)TornadoFX應(yīng)用程序

較新版本的JVM知道如何在沒(méi)有main()方法的情況下啟動(dòng)JavaFX應(yīng)用程序。 JavaFX應(yīng)用程序(TornadoFX應(yīng)用程序是其擴(kuò)展)桅咆,是繼承javafx.application.Application的任何類括授。 由于tornadofx.App繼承了javafx.application.Application ,TornadoFX應(yīng)用程序沒(méi)有什么不同岩饼。 因此荚虚,您將通過(guò)引用com.example.app.MyApp啟動(dòng)該應(yīng)用程序,并且您不一定需要一個(gè)main()函數(shù)籍茧,除非您需要提供命令行參數(shù)版述。 在這種情況下,您將需要添加一個(gè)包級(jí)別的主函數(shù)到MyApp.kt文件:

fun main(args: Array<String>) {
  Application.launch(MyApp::class.java, *args)
}

這個(gè)主函數(shù)將被編譯進(jìn)com.example.app.MyAppKt - 注意最后的Kt寞冯。 當(dāng)您創(chuàng)建包級(jí)別的主函數(shù)時(shí)渴析,它將始終具有完全限定包的類名,加上文件名吮龄,附加Kt俭茧。

對(duì)于啟動(dòng)和測(cè)試App ,我們將使用Intellij IDEA漓帚。 導(dǎo)航到Run→Edit Configurations (圖3.1)母债。

圖3.1

單擊綠色“+”符號(hào)并創(chuàng)建一個(gè)新的應(yīng)用程序配置(圖3.2)。

圖3.2

指定 “主類(Main class)” 的名稱尝抖,這應(yīng)該是您的App類场斑。 您還需要指定它所在的模塊(module)漓踢。給配置一個(gè)有意義的名稱,如 “Launcher”漏隐。 之后點(diǎn)擊 “OK”(圖3.3)喧半。

圖3.3

您可以通過(guò)選擇Run→Run 'Launcher'或任何您命名的配置來(lái)運(yùn)行 TornadoFX應(yīng)用程序(圖3.4)。

圖3.4

您現(xiàn)在應(yīng)該看到您的應(yīng)用程序啟動(dòng)了(圖3.5)

圖3.5

恭喜青责! 您已經(jīng)編寫了您的第一個(gè)(雖然簡(jiǎn)單)TornadoFX應(yīng)用程序挺据。 現(xiàn)在看起來(lái)可能不是很好,但是當(dāng)我們涵蓋更多TornadoFX的強(qiáng)大功能時(shí)脖隶,我們將創(chuàng)建大量令人印象深刻的用戶界面扁耐,幾乎沒(méi)有多少代碼,而且只需要很少時(shí)間产阱。 但首先讓我們來(lái)更好地了解AppView之間發(fā)生的情況婉称。

了解視圖(View)

讓我們深入了解View的工作原理以及如何使用它。 看看我們剛剛構(gòu)建的AppView類构蹬。

class MyApp: App(MyView::class)

class MyView: View() {
    override val root = VBox()

    init {
        with(root) {
            this += Button("Press Me")
            this += Label("Waiting")
        }
    }
}

View包含JavaFX節(jié)點(diǎn)的層次結(jié)構(gòu)王暗,并在它被調(diào)用的位置通過(guò)名稱注入。 在下一節(jié)中庄敛,我們將學(xué)習(xí)如何利用強(qiáng)大的構(gòu)建器(powerful builders)來(lái)快速創(chuàng)建這些Node層次結(jié)構(gòu)俗壹。TornadoFX維護(hù)的MyView只有一個(gè)實(shí)例,有效地使其成為單例藻烤。TornadoFX還支持范圍(scopes)绷雏,它們可以將ViewFragmentController的集合組合在一個(gè)單獨(dú)的命名空間中怖亭,如果你愿意的話涎显,那么View只能是該范圍內(nèi)的單例。 這對(duì)于多文檔接口應(yīng)用程序(Multiple-Document Interface applications)和其他高級(jí)用例非常有用兴猩。 稍后再說(shuō)棺禾。

使用inject()和嵌入視圖(Embedding Views)

您也可以將一個(gè)或多個(gè)視圖注入另一個(gè)View 。 下面我們將TopViewBottomView嵌入到MasterView峭跳。 請(qǐng)注意膘婶,我們使用inject()代理屬性(delegate property)來(lái)懶惰地注入TopViewBottomView實(shí)例。 然后我們調(diào)用每個(gè)child Viewroot來(lái)賦值給BorderPane(圖3.6)蛀醉。

class MasterView: View() {
    val topView: TopView by inject()
    val bottomView: BottomView by inject()

    override val root = borderpane {
        top = topView.root
        bottom = bottomView.root
    }
}

class TopView: View() {
    override val root = label("Top View")
}

class BottomView: View() {
    override val root = label("Bottom View")
}
圖3.6

如果您需要在視圖間彼此溝通悬襟,您可以在每個(gè)child View中創(chuàng)建一個(gè)屬性來(lái)保存parent View

class MasterView : View() {
    override val root = BorderPane()

    val topView: TopView by inject()
    val bottomView: BottomView by inject()

    init {
        with(root) {
            top = topView.root
            bottom = bottomView.root
        }

        topView.parent = this
        bottomView.parent = this
    }
}

class TopView: View() {
    override val root = Label("Top View")
    lateinit var parent: MasterView
}

class BottomView: View() {
    override val root = Label("Bottom View")
    lateinit var parent: MasterView
}

更通常地拯刁,您將使用ControllerViewModel在視圖之間進(jìn)行通信脊岳,稍后我們將訪問(wèn)此主題。

使用find()來(lái)注入

inject()代理(delegate)將懶惰地將一個(gè)給定的組件賦值給一個(gè)屬性。 第一次調(diào)用該組件時(shí)割捅,它將被檢索奶躯。 或者,不使用inject()代理亿驾,您可以使用find()函數(shù)來(lái)檢索View或其他組件的單例實(shí)例嘹黔。

class MasterView : View() {
    override val root = BorderPane()

    val topView = find(TopView::class)
    val bottomView = find(BottomView::class)

    init {
        with(root) {
            top = topView.root
            bottom = bottomView.root
        }
    }
}

class TopView: View() {
    override val root = Label("Top View")
}

class BottomView: View() {
    override val root = Label("Bottom View")
}

您可以使用find()inject() ,但是使用inject()代理是執(zhí)行依賴注入的首選方法莫瞬。

雖然我們將在下一章更深入地介紹構(gòu)建器(builders)儡蔓,但現(xiàn)在是時(shí)候來(lái)揭示上述示例可以用更加簡(jiǎn)潔明了的語(yǔ)法來(lái)編寫了:

class MasterView : View() {
    override val root = borderpane {
        top(TopView::class)
        bottom(BottomView::class)
    }
}

我們不是先注入TopViewBottomView,然后將它們各自的root節(jié)點(diǎn)賦值給BorderPanetopbottom屬性疼邀,而是使用構(gòu)建器語(yǔ)法(builder syntax喂江,全部小寫)來(lái)指定BorderPane,然后聲明性地告訴TornadoFX拉入兩個(gè)子視圖旁振,并使他們自動(dòng)賦值到topbottom屬性获询。 我們希望您會(huì)認(rèn)同,這是很具表現(xiàn)力的拐袜,具有少得多的樣板(boiler plate)吉嚣。 這是TornadoFX試圖以此為生的最重要的原則之一:減少樣板(boiler plate),提高可讀性阻肿。 最終的結(jié)果往往是更少的代碼和更少的錯(cuò)誤瓦戚。

控制器(Controllers)

在許多情況下沮尿,將UI分為三個(gè)不同的部分被認(rèn)為是一種很好的做法:

    1. 模型(Model) - 擁有核心邏輯和數(shù)據(jù)的業(yè)務(wù)代碼層丛塌。
    1. 視圖(View)- 具有各種輸入和輸出控件的視覺(jué)顯示。
    1. 控制器(Controller) - “中間人(middleman)” 介入(mediating)模型和視圖之間的事件畜疾。

還有其他的MVC流派赴邻,例如MVVM和MVP,所有這些都可以在TornadoFX中使用啡捶。

盡管您可以將模型和控制器的所有邏輯放在視圖之中姥敛,但是最好將這三個(gè)部分清楚地分開,以便最大程度地實(shí)現(xiàn)可重用性瞎暑。 一個(gè)常用的模式是MVC模式彤敛。 在TornadoFX中,可以注入一個(gè)Controller來(lái)支持View了赌。

這里給出一個(gè)簡(jiǎn)單的例子墨榄。 使用一個(gè)TextField創(chuàng)建一個(gè)簡(jiǎn)單的View ,當(dāng)一個(gè)Button被點(diǎn)擊時(shí)勿她,其值被寫入到一個(gè)“數(shù)據(jù)庫(kù)”袄秩。 我們可以注入一個(gè)處理與寫入數(shù)據(jù)庫(kù)的模型交互的Controller 。 由于這個(gè)例子是簡(jiǎn)化的,所以不會(huì)有實(shí)際的數(shù)據(jù)庫(kù)之剧,但打印的消息將作為占位符(圖3.7)郭卫。

class MyView : View() {
    val controller: MyController by inject()
    var inputField: TextField by singleAssign()

    override val root = vbox {
        label("Input")
        inputField = textfield()
        button("Commit") {
            action {
                controller.writeToDb(inputField.text)
                inputField.clear()
            }
        }
    }
}

class MyController: Controller() {
    fun writeToDb(inputValue: String) {
        println("Writing $inputValue to database!")
    }
}
圖3.7

當(dāng)我們構(gòu)建UI時(shí),我們確保添加對(duì)inputField的引用背稼,以便以后可以在“Commit”按鈕的onClick事件處理程序中引用贰军。 當(dāng)單擊“Commit”按鈕時(shí),您將看到控制器向控制臺(tái)打印一行雇庙。

Writing Alpha to database!

重要的是要注意谓形,雖然上述代碼是可工作的,甚至可能看起來(lái)也不錯(cuò)疆前,但是很好的做法是要避免直接引用其他UI元素寒跳。 如果您將UI元素綁定到屬性并操作屬性,那么您的代碼將更容易重構(gòu)竹椒。 稍后我們將介紹ViewModel童太,它提供了更簡(jiǎn)單的方法來(lái)處理這種類型的交互。

長(zhǎng)時(shí)間運(yùn)行的任務(wù)

每當(dāng)您在控制器中調(diào)用函數(shù)時(shí)胸完,需要確定該函數(shù)是否立即返回书释,或者執(zhí)行潛在的長(zhǎng)時(shí)間運(yùn)行的任務(wù)。 如果您在JavaFX應(yīng)用程序線程中調(diào)用函數(shù)赊窥,則UI將在響應(yīng)完成之前無(wú)響應(yīng)爆惧。 無(wú)響應(yīng)的UI是用戶感知(user perception)的殺手,因此請(qǐng)確保您在后臺(tái)運(yùn)行昂貴的操作锨能。 TornadoFX提供了runAsync功能來(lái)幫助您扯再。

放置在一個(gè)runAsync塊內(nèi)的代碼將在后臺(tái)運(yùn)行。 如果后臺(tái)調(diào)用的結(jié)果需要更新您的UI址遇,則必須確保您在JavaFX的應(yīng)用程序線程中應(yīng)用更改熄阻。ui區(qū)塊正是這樣。

val textfield = textfield()
button("Update text") {
    action {
        runAsync {
            myController.loadText()
        } ui { loadedText ->
            textfield.text = loadedText
        }
    }
}

當(dāng)單擊按鈕時(shí)倔约,將運(yùn)行action構(gòu)建器(將ActionEvent代理給setAction方法)中的操作秃殉。 它調(diào)用myController.loadText(),并當(dāng)它返回shi將結(jié)果應(yīng)用于textfieldtext屬性浸剩。 當(dāng)控制器功能運(yùn)行時(shí)钾军,UI保持響應(yīng)。

在表面以下绢要, runAsync會(huì)創(chuàng)建一個(gè)JavaFX的Task對(duì)象吏恭,并將創(chuàng)建一個(gè)單獨(dú)的線程以在Task里運(yùn)行你的調(diào)用。 您可以將此Task賦值給變量袖扛,并將其綁定到UI砸泛,以在運(yùn)行時(shí)顯示進(jìn)度十籍。

事實(shí)上,這是很常見(jiàn)的唇礁,為此還有一個(gè)名為TaskStatus的默認(rèn)ViewModel扎谎,它包含running 炎功,message筒饰,titleprogress等可觀察值晋柱。 您可以使用TaskStatus對(duì)象的特定實(shí)例來(lái)提供runAsync調(diào)用,或使用默認(rèn)值琢融。

TornadoFX源代碼在AsyncProgressApp.kt文件中包含一個(gè)示例用法界牡。

還有一個(gè)名為runAsyncWithProgressrunAsync版本, runAsync在長(zhǎng)時(shí)間運(yùn)行的操作運(yùn)行時(shí)漾抬,以進(jìn)度指示器來(lái)覆蓋當(dāng)前節(jié)點(diǎn)宿亡。

singleAssign()屬性代理

在上面的例子中,我們用singleAssign代理初始化了inputField屬性纳令。 如果要保證只賦值一次值挽荠,可以使用singleAssign()代理代替Kotlin的lateinit關(guān)鍵字。 這將導(dǎo)致第二個(gè)賦值引發(fā)錯(cuò)誤平绩,并且在賦值之前過(guò)早訪問(wèn)時(shí)也會(huì)出錯(cuò)圈匆。

您可以在附錄A1中詳細(xì)查看有關(guān)singleAssign()的更多信息,但是現(xiàn)在知道它保證只能賦值一次給var捏雌。 它也是線程安全的跃赚,有助于減輕可變性(mutability)問(wèn)題。

您還可以使用控制器向View提供數(shù)據(jù)(圖3.8)性湿。

class MyView : View() {
    val controller: MyController by inject()

    override val root = vbox {
        label("My items")
        listview(controller.values)
    }
}

class MyController: Controller() {
    val values = FXCollections.observableArrayList("Alpha","Beta","Gamma","Delta")
}
圖3.8

VBox包含一個(gè)Label和一個(gè)ListView纬傲,Controllervalues屬性被賦值給ListViewitems屬性。

無(wú)論他們是讀數(shù)據(jù)還是寫數(shù)據(jù)窘奏,控制器都可能會(huì)執(zhí)行長(zhǎng)時(shí)間運(yùn)行的任務(wù)嘹锁,從而不能在JavaFX線程上執(zhí)行任務(wù)葫录。 本章后面您將學(xué)習(xí)如何使用runAsync構(gòu)造來(lái)輕松地將工作卸載到工作線程着裹。

分段(Fragment)

您創(chuàng)建的任何View都是單例,這意味著您通常只能在一個(gè)地方一次使用它米同。 原因是在JavaFX應(yīng)用程序中View的根節(jié)點(diǎn)(root node)只能具有單個(gè)父級(jí)骇扇。 如果你賦值另一個(gè)父級(jí),它將從它的先前的父級(jí)消失面粮。

但是少孝,如果您想創(chuàng)建一個(gè)短暫(short-lived)的UI,或者可以在多個(gè)地方使用熬苍,請(qǐng)考慮使用Fragment稍走。 片段(Fragment)是可以有多個(gè)實(shí)例的特殊類型的View袁翁。 它們對(duì)于彈出窗口或更大的UI甚至是單個(gè)ListCell都特別有用。 稍后我們將會(huì)看到一個(gè)名為ListCellFragment的專門的片段婿脸。

ViewFragment支持openModal() 粱胜, openWindow()openInternalWindow() ,它將在單獨(dú)的窗口(Window)中打開根節(jié)點(diǎn)狐树。

class MyView : View() {
    override val root = vbox {
        button("Press Me") {
            action {
                find(MyFragment::class).openModal(stageStyle = StageStyle.UTILITY)
            }
        }
    }
}

class MyFragment: Fragment() {
    override val root = label("This is a popup")
}

您也可以將可選參數(shù)傳遞給openModal()以修改其一些行為焙压。

openModal()的可選參數(shù)

參數(shù) 類型 描述
stageStyle StageStyle 定義·Stage·可能的枚舉樣式之一。 默認(rèn)值: ·StageStyle.DECORATED·
modality Modality 定義Stage一個(gè)可能的枚舉模式類型抑钟。 默認(rèn)值: Modality.APPLICATION_MODAL
escapeClosesWindow Boolean 設(shè)置ESC鍵調(diào)用closeModal() 涯曲。 默認(rèn)值: true
owner Window 指定此階段的所有者窗口
block Boolean 阻止UI執(zhí)行,直到窗口關(guān)閉在塔。 默認(rèn)值: false

InternalWindow

盡管openModal在一個(gè)新的Stage打開幻件, openInternalWindow卻在當(dāng)前的根節(jié)點(diǎn)(current root node)或任何你指定的其他節(jié)點(diǎn)上打開:

 button("Open editor") {
        action {
            openInternalWindow(Editor::class)
        }
    }
圖3.9

內(nèi)部窗口(internal window)的一個(gè)很好的用例是單舞臺(tái)(single stage)環(huán)境(如JPro),或者如果要自定義窗口蛔溃,修剪該窗口使其看起來(lái)更符合你的應(yīng)用程序的設(shè)計(jì)傲武。 內(nèi)部窗口(Internal Window)可以使用CSS樣式。 有關(guān)樣式可更改(styleable)屬性的更多信息城榛,請(qǐng)查看InternalWindow.Styles類揪利。

內(nèi)部窗口(internal window)API在一個(gè)重要方面與模態(tài)/窗口(modal/window)不同。 由于窗口(window)在現(xiàn)有節(jié)點(diǎn)上打開狠持,您通常會(huì)在你想要其在上打開的View中調(diào)用openInternalWindow()疟位。 您提供要顯示的視圖(View),您也可以選擇通過(guò)owner參數(shù)提供要在其上打開的節(jié)點(diǎn)(node)喘垂。

openInternalWindow()的可選參數(shù)

參數(shù) 類型 描述
view UIComponent 組件將是新窗口的內(nèi)容
view KClass 或者甜刻,您可以提供視圖的類而不是實(shí)例
icon Node 可選的窗口圖標(biāo)
scope Scope 如果指定視圖類,則還可以指定用于獲取視圖的作用域
modal Boolean 定義在內(nèi)部窗口處于活動(dòng)狀態(tài)時(shí)是否應(yīng)該禁用被覆蓋節(jié)點(diǎn)正勒。 默認(rèn)值: true
escapeClosesWindow Boolean 設(shè)置ESC鍵調(diào)用close() 得院。 默認(rèn)值: true
owner Node 指定此窗口的所有者節(jié)點(diǎn)。 默認(rèn)情況下章贞,該窗口將覆蓋此視圖的根節(jié)點(diǎn)

關(guān)閉模式窗口

使用openModal() 祥绞, openWindow()openInternalWindow()打開的任何Component都可以通過(guò)調(diào)用closeModal()關(guān)閉。 如果需要使用findParentOfType(InternalWindow::class)也可以直接訪問(wèn)InternalWindow實(shí)例鸭限。

更換視圖和對(duì)接事件(Replacing Views and Docking Events)

使用TornadoFX蜕径,可以使用replaceWith()方便地與當(dāng)前View進(jìn)行交換,并可選擇添加一個(gè)轉(zhuǎn)換(transition)败京。 在下面的示例中兜喻,每個(gè)View上的Button將切換到另一個(gè)視圖,可以是MyView1MyView2 (圖3.10)赡麦。

class MyView1: View() {
    override val root = vbox {
        button("Go to MyView2") {
            action {
                replaceWith(MyView2::class)
            }
        }
    }
}

class MyView2: View() {
    override val root = vbox {
        button("Go to MyView1") {
            action {
                replaceWith(MyView1::class)
            }
        }
    }
}
圖3.10

您還可以選擇為兩個(gè)視圖之間的轉(zhuǎn)換指定一個(gè)精巧的動(dòng)畫朴皆。

replaceWith(MyView1::class, ViewTransition.Slide(0.3.seconds, Direction.LEFT)

這可以通過(guò)用另一個(gè)Viewroot替換給定View上的root帕识。 View具有兩個(gè)函數(shù)可以重載(override),用于在其root Node連接到父級(jí)( onDock() )以及斷開連接( onUndock() )時(shí)遂铡。 每當(dāng)View進(jìn)入或退出時(shí)渡冻,您可以利用這兩個(gè)事件進(jìn)行“連接”和“清理”。 運(yùn)行下面的代碼時(shí)您會(huì)注意到忧便,每當(dāng)View被交換時(shí)族吻,它將取消(undock )上一個(gè)View并停靠(dock )新的珠增。 您可以利用這兩個(gè)事件來(lái)管理初始化(initialization)和處理(disposal)任務(wù)超歌。

class MyView1: View() {
    override val root = vbox {
        button("Go to MyView2") {
            action {
                replaceWith(MyView2::class)
            }
        }
    }

    override fun onDock() {
        println("Docking MyView1!")
    }

    override fun onUndock() {
        println("Undocking MyView1!")
    }
}

class MyView2: View() {
    override val root = vbox {
        button("Go to MyView1") {
            action {
                replaceWith(MyView1::class)
            }
        }
    }

    override fun onDock() {
        println("Docking MyView2!")
    }
    override fun onUndock() {
        println("Undocking MyView2!")
    }
}

將參數(shù)傳遞給視圖

在視圖之間傳遞信息的最佳方式通常是注入ViewModel。 即使如此蒂教,可以將參數(shù)傳遞給其他組件仍然是很便利的巍举。 find()inject()函數(shù)支持Pair<String, Any>這樣的varargs,就可以用于此目的凝垛。 考慮在一個(gè)客戶列表中懊悯,為選定的客戶項(xiàng)打開客戶信息編輯器的情形。 編輯客戶信息的操作可能如下所示:

fun editCustomer(customer: Customer) {
    find<CustomerEditor>(mapOf(CustomerEditor::customer to customer).openWindow())
}

這些參數(shù)作為映射傳遞梦皮,其中鍵(key)是視圖中的屬性(property)炭分,值(value)是您希望的屬性的任何值。 這為您提供了一種配置目標(biāo)視圖參數(shù)的安全方式剑肯。

這里我們使用Kotlin的to語(yǔ)法來(lái)創(chuàng)建參數(shù)捧毛。 如果你愿意,這也可以寫成Pair(CustomerEditor::customer, customer)让网。 編輯器現(xiàn)在可以這樣訪問(wèn)參數(shù):

class CustomerEditor : Fragment() {
    val customer: Customer by param()

}

如果要檢查參數(shù)呀忧,而不是盲目依賴它們是可用的,您可以將其聲明為可空(nullable)溃睹,或參考其params映射:

class CustomerEditor : Fragment() {
    init {
        val customer = params["customer"] as? Customer
        if (customer != null) {
            ...
        }
    }
}

如果您不關(guān)心類型安全性而账,還可以將參數(shù)作為mapOf("customer" to customer)傳遞,但是如果在目標(biāo)視圖中重命名屬性因篇,則會(huì)錯(cuò)過(guò)自動(dòng)重構(gòu)(automatic refactoring)泞辐。

訪問(wèn)主舞臺(tái)(primary stage)

View具有一個(gè)名為primaryStage的屬性,允許您操作支持它的Stage的屬性惜犀,例如窗口大小铛碑。 通過(guò)openModal()打開的任何ViewFragment也將有一個(gè)modalStage屬性可用狠裹。

訪問(wèn)場(chǎng)景(scene)

有時(shí)需要從ViewFragment獲取當(dāng)前場(chǎng)景虽界。 這可以通過(guò)root.scene來(lái)實(shí)現(xiàn),或者如果你位于一個(gè)類型安全的構(gòu)建器(type safe builder)內(nèi)部涛菠,還有一個(gè)更短的方法莉御,只需使用scene撇吞。

訪問(wèn)資源(resources)

許多JavaFX API將資源作為URLURLtoExternalForm。 要檢索資源url礁叔,通常會(huì)如下所寫:

val myAudioClip = AudioClip(MyView::class.java.getResource("mysound.wav").toExternalForm())

每個(gè)Component都有一個(gè)resources對(duì)象牍颈,可以檢索resources的外部形式url(external form url),如下所示:

val myAudiClip = AudioClip(resources["mysound.wav"])

如果您需要一個(gè)實(shí)際的URL琅关,可以這樣檢索:

 val myResourceURL = resources.url( "mysound.wav" ) 

resources助手還有一些其他有用的功能煮岁,可幫助您將相對(duì)于Component的文件轉(zhuǎn)換為所需類型的對(duì)象:

val myJsonObject = resources.json("myobject.json")
val myJsonArray = resources.jsonArray("myarray.json")
val myStream = resources.stream("somefile")

值得一提的是, jsonjsonArray函數(shù)也可以在InputStream對(duì)象上使用涣易。

資源與Component相對(duì)應(yīng)画机,但您也可以通過(guò)完整路徑,從/開始檢索資源新症。

動(dòng)作的快捷鍵和組合鍵

您可以在鍵入某些組合鍵時(shí)觸發(fā)動(dòng)作(fire actions)步氏。 這是用shortcut函數(shù)完成的:

shortcut(KeyCombination.valueOf("Ctrl+Y")) {
    doSomething()
}

還有一個(gè)字符串版本的shortcut函數(shù)與此相同,但是不太冗長(zhǎng):

shortcut("Ctrl+Y")) {
    doSomething()
}

您還可以直接向按鈕操作添加快捷方式:

button("Save") {
    action { doSave() }
    shortcut("Ctrl+S")
}

觸摸支持

JavaFX對(duì)觸摸的支持開箱即用徒爹,現(xiàn)在唯一需要改進(jìn)的地方就是以更方便的方式處理shortpresslongpress荚醒。 它由兩個(gè)類似于action的函數(shù)組成,可以在任何Node上進(jìn)行配置:

shortpress { println("Activated on short press") }
longpress { println("Activated on long press") }

這兩個(gè)函數(shù)都接受consume參數(shù)隆嗅,默認(rèn)情況下為false界阁。 將其設(shè)置為true將防止按壓事件(press event)發(fā)生事件冒泡(event bubbling)。longpress函數(shù)還支持一個(gè)threshold參數(shù)胖喳,用于確定longpress積累的時(shí)間铺董。 默認(rèn)為700.millis

總結(jié)

TornadoFX充滿了簡(jiǎn)單禀晓,直觀而又強(qiáng)大的注入工具來(lái)管理視圖和控制器(Views and Controllers)精续。 它還使用Fragment簡(jiǎn)化對(duì)話框和其他小型UI。 盡管迄今為止粹懒,我們構(gòu)建的應(yīng)用程序非常簡(jiǎn)單重付,但希望您能欣賞到TornadoFX給JavaFX引入的簡(jiǎn)化概念。 在下一章中凫乖,我們將介紹可以說(shuō)是TornadoFX最強(qiáng)大的功能:Type-Safe Builders确垫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帽芽,隨后出現(xiàn)的幾起案子删掀,更是在濱河造成了極大的恐慌,老刑警劉巖导街,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件披泪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡搬瑰,警方通過(guò)查閱死者的電腦和手機(jī)款票,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門控硼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人艾少,你說(shuō)我怎么就攤上這事卡乾。” “怎么了缚够?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵幔妨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我谍椅,道長(zhǎng)陶冷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任毯辅,我火速辦了婚禮埂伦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘思恐。我一直安慰自己沾谜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布胀莹。 她就那樣靜靜地躺著基跑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪描焰。 梳的紋絲不亂的頭發(fā)上媳否,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音荆秦,去河邊找鬼篱竭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛步绸,可吹牛的內(nèi)容都是我干的掺逼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼瓤介,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吕喘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起刑桑,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤氯质,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后祠斧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闻察,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜓陌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片觅彰。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吩蔑,死狀恐怖钮热,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情烛芬,我是刑警寧澤隧期,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站赘娄,受9級(jí)特大地震影響仆潮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜遣臼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一性置、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧揍堰,春花似錦鹏浅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蝙眶,卻和暖如春季希,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背幽纷。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工式塌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人友浸。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓珊搀,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親尾菇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子境析,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容