MVC Groupの切り替えを分かった気になってやってみた。

元ネタはこちら。→ http://griffon.codehaus.org/FileViewer
Griffonは,MVCの組み合わせを複数持てるんだけど,あるMVCグループから別のMVCグループを生成するってのを試してみた。
スクリーンショットで見ると(↓)こんな感じに,JFrameを持つ大元の画面から,あるイベント(リストをダブルクリック)を起こすと,タブが増えていくってのを作ってみた。
  


大元の画面ってのと増えてくタブ(赤枠部分)ってのは,別々のMVCグループになってんのね。
#何作ろうとしているかは,ナイショだ。:-)


叩いたGriffonのコマンドはこれだけ。

> griffon create-app MvcApp
> cd MvcApp
> griffon create-mvc DetailPanel

これで,それぞれMvcApp,DetailPanelという接頭子を持つMVCの組み合わせができる。その定義情報は,$APP_HOME/griffon-app/conf/Application.groovy に記述される。

application {
  title='MvcApp'
  startupGroups = ['MvcApp']
}
mvcGroups {
  // MVC Group for "DetailPanel"
  DetailPanel {
    model = 'DetailPanelModel'
    view = 'DetailPanelView'
    controller = 'DetailPanelController'
  }

  // MVC Group for "Mvcapp"
  MvcApp {
    model = 'MvcAppModel'
    view = 'MvcAppView'
    controller = 'MvcAppController'
  }
}

create-appで作成したMVCグループが,application/startupGroupsに指定されてるんで,Griffonを実行(run-app)するとそのMVCグループが動くワケだな。


で,肝心のMVCグループの切り替え(つうか作成?)はどうするかっていうと,こうする。
#全部のコードはうしろのほうに載せとく。

= MvcAppController.groovyの一部抜粋 =
  def listClickedListener = { evt ->
      :
    createMVCGroup('DetailPanel', item, [rootPane:view.tab, item:item])
  }
}

createMVCGroup()の引数はこんな感じ。

  1. 新しく作るMVCグループの名前(Application.groovyに登録してある名前を指定)。
  2. MVCグループのインスタンス
    • ユニーク名がいいかどうかは用途次第
    • 指定した名前を使って,app.controllers.名前, app.models.名前, app.views.名前でそれぞれの要素の直接アクセスすることもできるそうだ。
  3. 指定したMVCグループに渡す引数

createMVCGroup()が呼び出されると,指定したMVCグループのコントローラのmvcGroupInit(Map)が呼び出される。もちっと具体的に言うと,こうだった。

MvcAppController
  createMVCGroup() ---> 1.new DetailPanelModel()
                        2.DetailPanelController.mvcGroupInit(Map)
                        3.DetailPanelView.build()

DetailPanelControllerのmvcGroupInit(Map)は,引数にMapを受け取るので連携が分かりやすいが,どうもこのMapの値,DetailPanelViewにも渡るみたいだった。
実際,DetailPanelViewのコード見るとわかるけど,変数:rootPaneitemはMvcAppControllerから渡ってきたものだ。

= DetailPanelView.groovyの一部抜粋 =
tabbedPane(id:'tab', rootPane, selectedIndex:rootPane.tabCount) {
  panel(title:item) {
    borderLayout()
      :

「わーい,これでMVCグループの切り替えもバッチリだ」とよろこんでみたものの幾つか謎が残る。

  • そもそもMVCグループを複数作る動機がわからん。:-)
  • できたMVCグループはいつ破棄されるのかがわからん。

あと一番最初の動くコントローラの初期化処理はどこでやるのが正しいんだろか?一応,MvcAppControllerにもmvcGroupInit(Map)があるから,そこに書いてみたけど,こんな例を良く見かける。
$APP_HOME/griffon-app/lifecycle/Startup.groovyに,

app.controllers.MvcApp.loadPages()

みたいに書いて,MvcAppControllerにloadPages()を定義する。

= MvcAppController.groovyの一部抜粋 =
  void loadPages() {
    // 初期化処理を書くぞー
      :
  }

まあ,Griffonもまだ0.1Betaだかんね。ここら辺の記述がこなれるには,もちょっとかかるんだろうな。
でも慣れてくるとスゴく気軽にGUI作れて,良い感じよ。>Griffon


どのこのリポジトリにも入れてないんで,せめてここにコードを残す。といってもコントローラとビューしか作ってないよ(モデルは使ってない)。


MvcAppController.groovy
EDTの使い方は,まったくもって当てずっぽう。いい加減,ちゃんと覚えねば。:-(

import javax.swing.*
import javax.swing.event.*
import javax.swing.tree.*

class MvcAppController {
  def model
  def view

  void mvcGroupInit(Map args) {
    doOutside {
      def contents = new DefaultMutableTreeNode("hudson")
      (1..10).each {
        def node = new DefaultMutableTreeNode("job-${it}")
        contents.add(node)
      }
      doLater {
        view.jobTree.model = new DefaultTreeModel(contents)
        view.jobTree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
        view.jobTree.addTreeSelectionListener( treeSelectionListener )

        view.buildList.mouseClicked = listClickedListener
      }
    }
  }

  def treeSelectionListener = { evt ->
    def path = evt.path
    if (path.pathCount == 2) {
      doLater {
        def node = view.jobTree.lastSelectedPathComponent
        if( !node ) return
        def listModel = new DefaultListModel()
        (1..10).each {
          listModel.addElement("${node.userObject}::build-${it}")
        }
        view.buildList.model = listModel
      }
    }
  } as TreeSelectionListener

  def listClickedListener = { evt ->
    if (evt.getClickCount() != 2) return
    def idx = view.buildList.locationToIndex(evt.point)
    if (idx < 0) return
    def item = view.buildList.model.elementAt(idx)
    createMVCGroup('DetailPanel', item, [rootPane:view.tab, item:item])
  }
}

MvcAppView.groovy
SwingBuilderにおけるJTabbedPaneの使い方がよくわからんかった。

import static java.awt.BorderLayout.*

application(title:'mvc-app', size:[500,400], locationByPlatform:true) {
  tabbedPane(id:'tab') {
    splitPane(title:"jobs") {
      panel {
        borderLayout()
        scrollPane(constraints:CENTER) {
          tree(id:'jobTree')
        }
      }
      panel {
        borderLayout()
        scrollPane(constraints:CENTER) {
          list(id:'buildList')
        }
      }
    }
  }
}


DetailPanelController.groovy

import javax.swing.*
import javax.swing.tree.*
import javax.swing.event.*

class DetailPanelController {
  def model
  def view

  void mvcGroupInit(Map args) {
    doOutside {
      def contents = new DefaultMutableTreeNode(args.item)
      (1..20).each {
        def node = new DefaultMutableTreeNode("${args.item}::test-${it}")
        contents.add(node)
      }
      doLater {
        view.testTree.model = new DefaultTreeModel(contents)
        view.testTree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
        view.testTree.addTreeSelectionListener( treeSelectionListener )
      }
    }
  }

  def treeSelectionListener = { evt ->
    def path = evt.path
    if (path.pathCount == 2) {
      doLater {
        def node = view.testTree.lastSelectedPathComponent
        if( !node ) return
        def writer = new StringWriter()
        (1..100).each {
          writer << "${node.userObject}::console-${it}\n"
        }
        view.consoleText.text = writer.toString()
      }
    }
  } as TreeSelectionListener

  def closeTab = {
    def tabIndex = view.tab.selectedIndex
    if (tabIndex < 0) return
    view.tab.remove(tabIndex)
    view.tab.selectedIndex = 0
  }
}

DetailPanelView.groovy

import java.awt.*
import static java.awt.BorderLayout.*

tabbedPane(id:'tab', rootPane, selectedIndex:rootPane.tabCount) {
  panel(title:item) {
    borderLayout()
    panel(constraints:NORTH) {
      button('CLOSE', actionPerformed:controller.closeTab)
    }
    panel(constraints:CENTER) {
      borderLayout()
      splitPane(constraints:CENTER) {
        panel {
          borderLayout()
          scrollPane(constraints:CENTER) {
            tree(id:'testTree')
          }
        }
        panel {
          borderLayout()
          scrollPane(constraints:CENTER) {
            textArea(id:'consoleText', editable:false,
                     font:new Font('Monospaced', Font.PLAIN, 12))
          }
        }
      }
    }
  }
}