LoginSignup
1
3

More than 5 years have passed since last update.

Kotlin / JavaFX 検索可能なTreeViewの実装例

Posted at

環境

  • Kotlin 1.0.5
  • JDK 1.8.0_121
  • ControlsFX 8.40.12

検索可能なTreeView

検索フィールド付きのTreeViewです。設定項目が多いIDEの設定ダイアログなどでよく採用されています。
3.gif

今回はTreeViewやTreeItemを継承せずに外から操作する仕組みで実装してみます。

使用例

Controller.kt
class Controller {

    val root: TreeItem<String>

    init {
        val items1 = arrayOf("Appearance",
                "Menus and Toolbars",
                "System Settings",
                "File Colors",
                "Scopes",
                "Notifications",
                "Quick Lists",
                "Path Variables")
        val items2 = arrayOf("Passwords",
                "HTTP Proxy",
                "Updates",
                "Usage Statistics")
        root = TreeItem<String>("Appearance & Behavior")
        root.isExpanded = true
        root.children.addAll(items1.map{TreeItem(it)})
        root.children[1].isExpanded = true
        root.children[1].children.addAll(items2.map { TreeItem(it) })
    }

    @FXML lateinit private var treeView: TreeView<String>
    @FXML lateinit private var searchField: CustomTextField  //  ...[1]

    @FXML fun initialize() {
        val icon = javaClass.getResource("search.png").openStream().use { Image(it) }
        searchField.left = ImageView(icon)
        treeView.root = root
        TreeItemFilter(root).bind( searchField.textProperty() )  // ...[2]
    }

}

上半分は普通のTreeViewにデータを突っ込む標準的な実装です。

  1. ControlsFXのCustomTextFieldを使用しました。検索フィールドの虫眼鏡アイコンを表示したかっただけなのでTextFieldと差し替えても特に変わりありません。

  2. TreeItemFilterというヘルパークラスを作りました。このヘルパークラスは検索フィールドのテキストが変更される度に、TreeItemの木構造から一致しないものを取り除いたり一致するものを加えたりする役割を持ちます。

TreeItemFilterクラス

TreeItemFilter.kt
import javafx.beans.value.ChangeListener
import javafx.beans.value.ObservableStringValue
import javafx.scene.control.TreeItem
import java.util.function.Predicate

class TreeItemFilter<T>(root: TreeItem<T>) {

    private data class Relation<T>(val parent: TreeItem<T>, val items: List<TreeItem<T>>)
    private val relations: List<Relation<T>>
    private var _searchProperty: ObservableStringValue? = null
    val searchProperty: ObservableStringValue?
    get() {return _searchProperty}
    var predicate: Predicate<TreeItem<T>>
    val changeListener = ChangeListener<String> { observableValue, oldV, nweV -> updateRelations() }

    init {
        relations = buildRelations(root)
        predicate = Predicate { it.value.toString().toLowerCase().contains(searchProperty?.value?.toLowerCase() ?: "") }
    }

    fun bind(searchProperty: ObservableStringValue) {  // ...[5]
        _searchProperty = searchProperty
        searchProperty.addListener( changeListener )
    }

    fun unbind(searchProperty: ObservableStringValue) {
        searchProperty.removeListener( changeListener )
    }

    private fun buildRelations(item: TreeItem<T>): List<Relation<T>> {    // ...[3]
        val list = mutableListOf<Relation<T>>()
        item.children.forEach {list.addAll(buildRelations(it))}
        if (item.children.isNotEmpty()) list.add(Relation(item, item.children.toTypedArray().toList()))

        return list
    }

    private fun updateRelations() {  // ...[4]
        relations.forEach { relation ->
            var index = 0
            relation.items.forEach { item ->
                val matched = predicate.test(item)
                val found = (relation.parent.children.find { it === item } != null)

                if (matched || item.children.isNotEmpty()) {
                    if (!found) relation.parent.children.add(index, item)
                    index++
                } else {
                    relation.parent.children.remove(item)
                }
            }
        }
    }
}

3.このクラスでは最初に各アイテムと親アイテムの関係をリストにして覚えておきます。ネストが深いほど先にリストアップしておくことで、次の工程でツリーの葉っぱの方から処理を行うことが出来るようになります。

4.[3]で覚えた順に検索文字列とのマッチングを行っていき、一致するものは親アイテムに追加して一致しなければ親アイテムから削除します。TreeView は TreeItem の構造が変化する度に見た目を更新するように実装されているのでViewが更新されるのはこのタイミングです。

5.検索フィールドのテキストの変化に追従して[4]を呼び出す仕組みです。

型引数を変更する場合の使用例

TreeItemの中身を好きなクラスに変える場合は検索文字列とのマッチング方法を教えてあげる必要があります。デフォルトではtoStringの内容と比較するようにしています。

Controller.kt
class Controller {
    val root: TreeItem<YourClass>

    // -- 中略 --

    @FXML lateinit private var treeView: TreeView<YourClass>

    // -- 中略 --

        val filter = TreeItemFilter(root)
        // YoutClass.yourTextValue と searchField.text を比較してBooleanを返す
        // 関数オブジェクトをセットしてあげます
        filter.predicate = Predicate { it.value.yourTextValue.toLowerCase().contains(searchField.text.trim().toLowerCase()) }
        filter.bind( searchField.textProperty() )

TreeViewの中でどう表示するかもCellFactory等を使って実装する必要がありそうですが、以下の解説を参考にされるとよいでしょう。

JavaFX 8 API: クラスTreeView
JavaFX TreeViewのカスタマイズ - その1:TreeItemのデータクラスの作成

参考

Filtering a JavaFX TreeView 違った方針のJava実装があります
ControlsFX: sample
ソース: GIT

FXML

Main.xml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TreeView?>
<?import javafx.scene.layout.BorderPane?>
<?import org.controlsfx.control.textfield.CustomTextField?>


<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="searchtreeview.Controller">
   <center>
      <TreeView fx:id="treeView" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
   </center>
   <top>
      <CustomTextField fx:id="searchField" promptText="Search" BorderPane.alignment="CENTER" />
   </top>
</BorderPane>
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3