JavaFXでコントローラー同士を参照する

やってみればなんてことはないんですが。
Controllerから別のControllerの処理を呼びたいのだけどどうすればいいかわからなかった。
自分的な答えは、コントローラー自身を他のコントローラーに渡しておく、ということでした。

ただ、コントローラーが他のコントローラーを持っておくには、ルールがあるようです。

http://stackoverflow.com/questions/14790785/subcontroller-not-being-injected-into-main-controller

ここによると、FXMLで、includeタグのfx:idに付けた値 + Controllerという名前のコントローラーじゃないとクラスが初期化時に保持してくれないです。
ここが重要のようなので、ミスしないようにしましょう。

<?xml version="1.0" encoding="UTF-8"?>
<VBox xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.example.Controller">
  <children>
    <fx:include fx:id="menuBar" source="menu_bar.fxml"/>
    <TableView fx:id="table">
      <columns>
        <TableColumn text="No." fx:id="idColumn" />
        <TableColumn text="名前" fx:id="nameColumn" />
        <TableColumn text="E-mail" fx:id="emailColumn" />
      </columns>
    </TableView>
    <!-- 略 -->
  </children>
</VBox>

次に、includeするmenu_bar.fxml。
MenuBarControllerのhogeメソッドに紐づいてます。

<?xml version="1.0" encoding="UTF-8"?>
<MenuBar
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.example.MenuBarController">
    <menus>
        <Menu mnemonicParsing="false" text="ファイル">
            <items>
                <MenuItem onAction="#hoge" mnemonicParsing="false" text="hoge" />
            </items>
        </Menu>
    </menus>
</MenuBar>

次に、プロパティ(Javaのメンバ変数)にMenuBarControllerを持つControllerのコード。
ただし、コードはKotlinです。

class Controller : Initializable {
  // FXMLはJavaのアノテーション@FXMLと同じ意味
  FXML private var menuBarController : MenuBarController? = null
  val hoge = "hoge"

  public override fun initialize(p0: URL?, p1: ResourceBundle?) {
    // 初期化の際に自身を渡しておく
    menuBarController!!.controller = this
  }
}

最後に、MenuBarController。Controllerクラスのプロパティhogeにアクセスさせてます。

class MenuBarController(){
  var controller : Controller by Delegates.notNull()
  FXML fun hoge(event: ActionEvent) {
    println(controller.hoge)
  }
}

相互にコントローラーを参照しているので、お互いのメソッドを呼び合うことができます。
あまりにコントローラーで参照しあうと複雑になりそうですが、2階層程度ならなんとかなりそうですね。


KotlinでORMLiteのdropTableができない件

データベースの抽象化ライブラリとしてORMLiteを使ってみてるんですが、KotlinでORMLiteを使ってTableUtils.dropTableをしようとしたらエラーでコンパイルが通りませんでした。

問題のソースはこれ。16行目がなぜかエラーになって通りません。

object DatabaseHelper {
    val connectionSource by Delegates.lazy {
        val databaseUrl = "jdbc:h2:~/test"
        JdbcConnectionSource(databaseUrl)
    };

    {
        setUpDatabase(connectionSource)
    }

    private fun setUpDatabase(connectionSource: ConnectionSource) {
        TableUtils.createTableIfNotExists(connectionSource, javaClass<User>())
    }

    fun truncate() {
        TableUtils.dropTable(connectionSource, javaClass<User>(), true)
        TableUtils.createTable(connectionSource, javaClass<User>())
    }

    fun getUserDao() : Dao<User, Int> = DaoManager.createDao(connectionSource, javaClass<User>())!!

    fun close() = connectionSource.close()
}

エラーメッセージは、

Type inference failed: Not enough information to infer parameter ID in

fun dropTable
(
connectionSource: com.j256.ormlite.support.ConnectionSource?,
dataClass: java.lang.Class?,
ignoreErrors: jet.Boolean
)
: jet.Int
Please specify it explicitly.

と出ていて、パラメータ IDの情報がないよということっぽいのだけれど、これどこで指定するのん?という話で、よくわかりません。。。(Java詳しくないので)

仕方がないので、Javaでtruncateメソッドを実装しました。

public class Truncate {
    public static void truncate(ConnectionSource connectionSource) throws SQLException {
        TableUtils.dropTable(connectionSource, User.class, true);
        TableUtils.createTable(connectionSource, User.class);
    }
}

で、これをKotlinで呼び出すと。

fun truncate() = Truncate.truncate(connectionSource)

うーむ、これなら動くけど、なんだか釈然としない。
Kotlinのバグなんだろうか?


JavaFXのFXMLでfx:includeを使ったときの罠

JavaFX + Kotlinでアプリを作っている最中なのですが、ハマったのでメモ。
JavaFXのSceneBuilderでズバッと部品を置いていくとFXMLができてくれるのですが、あまりにも長くなると読みにくいので、パーツ化していこうと思ってメニューをfx:includeで切り出しました。

最初のコードはこんな感じ。見通しをよくするために属性を削ってますので注意。
main.fxml(fx:controllerでcom.example.Controllerを指定)

<?xml version="1.0" encoding="UTF-8"?>
<VBox xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.example.Controller">
  <children>
    <fx:include source="menu_bar.fxml"/>
    <TableView fx:id="table">
      <columns>
        <TableColumn text="No." fx:id="idColumn" />
        <TableColumn text="名前" fx:id="nameColumn" />
        <TableColumn text="E-mail" fx:id="emailColumn" />
      </columns>
    </TableView>
    <!-- 略 -->
  </children>
</VBox>

menu_bar.fxml(同じく、fx:controllerでcom.example.Controllerを指定)

<?xml version="1.0" encoding="UTF-8"?>
<MenuBar
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.example.Controller">
    <!-- 略 -->
</MenuBar>

TableViewを使っているので、テーブルを表示しようとController#initializeメソッド内でテーブルを初期化しようとしたところ、ぬるぽで落ちました。
私は単純に、includeはFXMLの中にFXMLを埋め込むだけのものだろうと思っていたのですが、どうやら違うようです。

どうもfxmlファイル毎に初期化処理が行われるようで、2つのfxmlファイルが1つのコントローラーを指定していたらinitializeメソッドが正常に機能しないようです。
ここでは、main.fxmlとmenu_bar.fxmlで別々にControllerを定義する必要があります。

menu_bar.fxml(fx:controllerでcom.example.MenuControllerを指定)

<?xml version="1.0" encoding="UTF-8"?>
<MenuBar
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.example.MenuController">
    <!-- 略 -->
</MenuBar>

そして、メニュー用のコントローラーを作ればOK。
1つのコントローラーではダメなんですねぇ…。


Kotlinでクラスの処理を委譲する

※2013/07/12現在、これをやると、処理は委譲できるんだけど、IntelliJのコード補完が死ぬみたいなので、オススメできないです。すみません。

== 以下、原文。

Kotlinのサイトで委譲(Delegation)の説明があるのだけれど、その方法が、渡された引数にたいしてのものだったのでよくわからなかった。というかそのパターンはそうなんだろうけど、俺がやりたいのはそうじゃないんだよ、ってことだった。

いま、KotlinでORMLiteを使っていて、DAOの実装をテーブル単位で行いたいからどうしようかなと思っていた。
とりあえず、DAOクラスはシングルトンでよかろうと思ってobjectで実装していた。

まずDatabaseHelperのソース。
Delegates.lazyを使って遅延評価してる。使われるときになってDBに接続するようにしてみた。
Userはエンティティクラスなので記載を省略する。

object DatabaseHelper {
    val connectionSource by Delegates.lazy {
        val databaseUrl = "jdbc:h2:~/test"
        JdbcConnectionSource(databaseUrl)
    };

    {
        setUpDatabase(connectionSource)
    }

    private fun setUpDatabase(connectionSource: ConnectionSource) {
        TableUtils.createTableIfNotExists(connectionSource, javaClass<User>())
    }

    fun getUserDao() : Dao<User, Int> = DaoManager.createDao(connectionSource, javaClass<User>())!!

    fun close() = connectionSource.close()
}

次に、UserDaoクラスを作った。

object UserDao {
    public val dao : Dao<User, Int> by Delegates.lazy {
        DatabaseHelper.getUserDao()
    }

    fun existsUser(user: User) : Boolean {
        val where = dao.queryBuilder()!!.where()!!
        if (user.id != null) {
            where.ne(User.ID, user.id)!!.and()
        }
        return where.eq(User.EMAIL, user.email)!!.countOf() > 0
    }

    fun create(user: User) {
        dao.create(user)
    }
}

このcreateメソッドが、daoに処理を任せている。update,deleteなど、これからたくさん同じようなことが起きるので、daoプロパティに処理を任せてしまいたい。
そう思ってDeletationを調べだした。で、最終的にたどり着いたコードがこちら。

object UserDao : Dao<User, Int> by DatabaseHelper.getUserDao() {
    
    fun existsUrl(user: User) : Boolean {
        val where = queryBuilder()!!.where()!!
        if (user.id != null) {
            where.eq(User.ID, user.id)!!.and()
        }
        return where.eq(User.URL, user.url)!!.countOf() > 0
    }
}

クラス名の後ろにbyを付ける事で、委譲対象を指定できるようだ。別に引数で受け取ったインスタンスである必要もないみたいだ。
まぁ正しいやり方かどうかってのが怪しい。。。
しかし、これでUserDao.create(user)みたいに呼びだすことができたので、問題はなさそうかなと。
全部委譲したくない場合はどうするんだろうか?
最初のようにやっていくのかな?


KotlinのTraitでクラスに依存した実装を行う。

AndroidのLogとかToastとかよく使うから短縮用のメソッドとかをActivityを継承したBaseActivityを作るのがバッドノウハウとか、ようてんさんが言ってたので、じゃあKotlinのtraitでやってみりゃいいんじゃねーの?と思ったのでやってみた。

まずはLogTraitを実装してみる。

package com.example.kotlin_trait
import android.util.Log

public trait LogTrait {
    fun e(tag: String, msg: String, e: Exception? = null) {
        if (e != null) {
            Log.e(tag, msg, e)
        } else {
            Log.e(tag, msg)
        }
    }

    fun i(tag: String, msg: String, e: Exception? = null) {
        if (e != null) {
            Log.i(tag, msg, e)
        } else {
            Log.i(tag, msg)
        }
    }

    fun v(tag: String, msg: String, e: Exception? = null) {
        if (e != null) {
            Log.v(tag, msg, e)
        } else {
            Log.v(tag, msg)
        }
    }

    fun d(tag: String, msg: String, e: Exception? = null) {
        if (e != null) {
            Log.d(tag, msg, e)
        } else {
            Log.d(tag, msg)
        }
    }

    fun w(tag: String, msg: String, e: Exception? = null) {
        if (e != null) {
            Log.w(tag, msg, e)
        } else {
            Log.w(tag, msg)
        }
    }
}

まぁここまでは普通のTraitですよねー。

次に、ToastTraitを実装してみる。
ToastTraitは、Toastを呼び出すのですが、ToastはContextを渡さないといけないんだけど、
それどーするの?というところで悩んでたんだけど、TraitにContextを継承させればいいようだ。
Traitでクラスを継承とかできるんすねー。知らんかった。

package com.example.kotlin_trait

import android.widget.Toast
import android.content.Context

public trait ToastTrait : Context {
    fun shortToast(resId : Int) {
        Toast.makeText(this, resId, Toast.LENGTH_SHORT)!!.show()
    }

    fun shortToast(str : String) {
        Toast.makeText(this, str, Toast.LENGTH_SHORT)!!.show()
    }

    fun longToast(resId : Int) {
        Toast.makeText(this, resId, Toast.LENGTH_LONG)!!.show()
    }

    fun longToast(str : String) {
        Toast.makeText(this, str, Toast.LENGTH_LONG)!!.show()
    }
}

こうすると、Traitの中のthisは継承したクラス自身になるので、
this=Contextオブジェクトになります。

これらをActivity側で継承する。

package com.example.kotlin_trait

import android.app.Activity
import android.os.Bundle

class MyActivity() : Activity(), LogTrait, ToastTrait {
    val TAG = getClass().getSimpleName()

    public override fun onCreate(savedInstanceState : Bundle?) {
        super<Activity>.onCreate(savedInstanceState)
        setContentView(R.layout.main)
        v(TAG, "v")
        d(TAG, "d")
        i(TAG, "i")
        w(TAG, "w")
        e(TAG, "e")
        shortToast("トースト食べたい。")
    }
}

ログは出せたし、Toastも出せた。これで勝つる。
やっぱりKotlinはいいですね。