TornadoFX JavaFX 同步跨表视图滚动

TornadoFX JavaFX Sync Scroll across tableviews

我正在尝试同步跨 table 视图的滚动。 (水平和垂直)

SyncScrollEx View 有两个 tableView,基本上是一个 Fragment 并排放置,具有相同的数据集,因此具有相同的 table 大小布局。

预期行为:当我在一个 table 视图上滚动时,另一个 table 视图的滚动条也应该滚动相同的量。

以下是我目前的进度:

import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.scene.control.ScrollBar
import tornadofx.*

class SyncScrollEx : View() {
    override val root = hbox {
        setPrefSize(300.0, 150.0)
        this += find<MyTableFrag>()
        this += find<MyTableFrag>()
    }
}
class MyTableFrag : Fragment() {
    var addEventOnlyOnceFlag = false
    val persons = FXCollections.observableArrayList<GameWarrior>(
            GameWarrior(1,"Tyrion Lannister", "M"),
            GameWarrior(2,"Ned Stark", "M"),
            GameWarrior(3,"Sansa Stark", "F"),
            GameWarrior(4,"Daenerys Targaryen", "F"),
            GameWarrior(5,"Bran Stark", "M"),
            GameWarrior(6,"Jon Snow", "M"),
            GameWarrior(7,"Arya Stark", "F")
    )
    override val root = vbox {
        tableview(persons) {
            column("ID", GameWarrior::idProperty)
            column("Name", GameWarrior::nameProperty)
            column("Gender", GameWarrior::genderProperty)
            subscribe<SyncScrollEvent> { event ->
                //Sync the ScrollX & ScrollY of both the tables
                event.node.value = event.newVal.toDouble()
            }
            //Hack, need to initialize this when the table/scroll is rendered
            setOnMouseEntered {
                //Hack for not triggering the lookupAll event on every mouse enter
                if (!addEventOnlyOnceFlag) {
                    addEventOnlyOnceFlag = true
                    //INFO: Look up for the scroll bars in tableView and add a listener
                    this.lookupAll(".scroll-bar").map { node ->
                        if (node is ScrollBar) {
                            node.valueProperty().addListener {
                                value, oldValue, newValue ->
                                println(node.orientation.toString() + " " + newValue)
                                fire(SyncScrollEvent(node, newValue))
                            }
                        }
                    }
                }
            }
        }
    }
}
class GameWarrior(id: Int, name: String, gender: String) {

    val idProperty = SimpleIntegerProperty(id)
    var id by idProperty

    val nameProperty = SimpleStringProperty(name)
    var name by nameProperty

    val genderProperty = SimpleStringProperty(gender)
    var gender by genderProperty
}
class SyncScrollEvent(val node: ScrollBar, val newVal: Number) : FXEvent()

评论突出了我面临的问题。
另外,我不明白在 Fire() 发生在 EventListener

中的情况下,"subscribe" 将如何为两个 table 视图调用

首先我们需要干净地访问滚动条。当 TableView 被分配给它的皮肤时,滚动条将可用。我们将创建一个以方向为键的地图来跟踪它们:

val scrollbars = HashMap<Orientation, ScrollBar>()

皮肤可用后,我们查找滚动条并将它们分配给我们的地图并监听变化,以便触发事件

skinProperty().onChange {
    this.lookupAll(".scroll-bar").map { it as ScrollBar }.forEach { bar ->
        scrollbars[bar.orientation] = bar
        bar.valueProperty().onChange {
            fire(SyncScrollEvent(bar, this))
        }
    }
}

我们不需要事件中的位置,因为我们可以查询滚动条的值,但如果我们添加源 TableView,则更容易过滤掉事件。 SyncScrollEvent 现在看起来像这样:

class SyncScrollEvent(val scrollbar: ScrollBar, val table: TableView<*>) : FXEvent()

让我们监听滚动事件,并确保仅当事件源自其他 tableview 时我们才更改滚动条值,以用于相应的方向:

subscribe<SyncScrollEvent> { event ->
    if (event.table != this)
        scrollbars[event.scrollbar.orientation]?.value = event.scrollbar.value
}

为了完整性,这里是整个修改后的应用程序:

import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.geometry.Orientation
import javafx.scene.control.ScrollBar
import javafx.scene.control.TableView
import tornadofx.*
import java.util.*

class SyncScrollEx : View() {
    override val root = hbox {
        setPrefSize(300.0, 150.0)
        add(MyTableFrag::class)
        add(MyTableFrag::class)
    }
}

class MyTableFrag : Fragment() {
    val persons = FXCollections.observableArrayList<GameWarrior>(
            GameWarrior(1, "Tyrion Lannister", "M"),
            GameWarrior(2, "Ned Stark", "M"),
            GameWarrior(3, "Sansa Stark", "F"),
            GameWarrior(4, "Daenerys Targaryen", "F"),
            GameWarrior(5, "Bran Stark", "M"),
            GameWarrior(6, "Jon Snow", "M"),
            GameWarrior(7, "Arya Stark", "F")
    )

    val scrollbars = HashMap<Orientation, ScrollBar>()

    override val root = vbox {
        tableview(persons) {
            column("ID", GameWarrior::idProperty)
            column("Name", GameWarrior::nameProperty)
            column("Gender", GameWarrior::genderProperty)
            subscribe<SyncScrollEvent> { event ->
                if (event.table != this)
                    scrollbars[event.scrollbar.orientation]?.value = event.scrollbar.value
            }
            skinProperty().onChange {
                this.lookupAll(".scroll-bar").map { it as ScrollBar }.forEach { bar ->
                    scrollbars[bar.orientation] = bar
                    bar.valueProperty().onChange {
                        fire(SyncScrollEvent(bar, this))
                    }
                }
            }
        }
    }
}

class GameWarrior(id: Int, name: String, gender: String) {

    val idProperty = SimpleIntegerProperty(id)
    var id by idProperty

    val nameProperty = SimpleStringProperty(name)
    var name by nameProperty

    val genderProperty = SimpleStringProperty(gender)
    var gender by genderProperty
}

class SyncScrollEvent(val scrollbar: ScrollBar, val table: TableView<*>) : FXEvent()