どうも Tweak のイベントなる物が納得いかないので小さなプログラムを作ってみる。納得いかないのは値の更新の方法。例えばある数字があって、それをテキストとスクロールバーの両方で現したい。テキストの方を変えるとスクロールバーが動き、スクロールバーが動くとテキストも動くという良くある状況。MVC 的には、モデルが「数値」でスクロールバーとテキストはそれに依存して動くビューになる。
特にフレームワークの都合で GUI 部品もそれぞれ「値」を持っている場合、それぞれの「値」を上手く同期してやる必要がある。その為に、モデル、GUI のそれぞれの変更を通知しあうのだが、下手うつと無限ループになる。
解決法は幾つもあるだろう。
- Morphic のプラガブル。そもそも参照する値はモデルの値だけ。関数的。
- Morphic のプーリング。一定時間ごとに値を調べる
- MVC。 設定(value: もしくは setValue: (手動通知)) と通知(changed update) により情報の流れを一方通行にする。モデルはGUI部品を知らない。GUI部品はGUIのビューを知らない。
Tweak の場合はイベント発火が変数アクセスに組み込みで、どんな更新も必ず通知される。組み込みGUIを使ってかつ無限ループを避けるには、イベント処理の先頭で今の値と同じかどうかを調べるしか無い。たしかにこれで動くのだが、なーんとなくイヤな感じがするのは何故だろう。
追記
大島さんの意見を参考に書き換えました。(ソースを読む人がいるとは思えないけど、後のメモに役立つため。) 一見単純なソースですが、ここまでたどり着く為の Tweak ならではの落とし穴がこれだけあります。なんでこんなに複雑なんだ。。。
- value := (なんとか) は value: (なんとか) の構文糖である。
- イベントハンドラ <on: valueChanged in: input> はイベント input value := (なんとか) 実行時必ず呼ばれる。
- イベントハンドラは、イベント実行後、つまり今のプロセス終了後呼ばれる。
- pouseScript: でイベントハンドラを止める事が出来るが、止める事が出来るのは今のプロセス中だけ。つまり、ある代入文がイベントを発生し、そのイベントハンドラでさらに新たなイベントが発生するのを抑制出来ない。(ドロップダウンリストで起こる)
- valueChanged (やその親戚)が発生するのは値が変わったとき。つまり、古い値 == 新しい値 の時は発生しない。これを利用して双方向制約時の無限ループを防ぐ事も出来る。
'From Squeak3.8gamma of ''24 November 2004'' [latest update: #6662] on 8 May 2005 at 10:00:32 am'! CPlayer subclass: #WidgetTest instanceVariableNames: '<?xml version="1.0" ?> <fields> <field toGet="input" toSet="input:" changeEvent="inputChanged">input</field> <field toGet="scroll" toSet="scroll:" changeEvent="scrollChanged">scroll</field> </fields>' classVariableNames: '' poolDictionaries: '' category: 'WidgetTest'! !WidgetTest commentStamp: 'tak 5/8/2005 00:10' prior: 0! WidgetTest new openInHnd! !WidgetTest methodsFor: 'accessing' stamp: 'tak 5/7/2005 14:09'! input "Answer the input of the receiver" <bewareOf: #inputChanged> ^self propertyValueAt: #input! ! !WidgetTest methodsFor: 'accessing' stamp: 'tak 5/7/2005 14:09'! input: aValue "Modify the receiver's input" ^self propertyValueAt: #input put: aValue with: #inputChanged! ! !WidgetTest methodsFor: 'accessing' stamp: 'tak 5/7/2005 11:11'! scroll "Answer the scroll of the receiver" <bewareOf: #scrollChanged> ^self propertyValueAt: #scroll! ! !WidgetTest methodsFor: 'accessing' stamp: 'tak 5/7/2005 11:11'! scroll: aValue "Modify the receiver's scroll" ^self propertyValueAt: #scroll put: aValue with: #scrollChanged! ! !WidgetTest methodsFor: 'accessing' stamp: 'tak 5/8/2005 09:49'! value: aValue super value: ((aValue asNumber asFloat truncateTo: 0.01) min: 100)! ! !WidgetTest methodsFor: 'events' stamp: 'tak 5/8/2005 09:58'! onValueChanged < on: valueChanged > input contents: value asString. scroll value: value! ! !WidgetTest methodsFor: 'events' stamp: 'tak 5/8/2005 10:00'! onValueChangedInInput < on: valueChanged in: input > value := input contents. ! ! !WidgetTest methodsFor: 'events' stamp: 'tak 5/8/2005 10:00'! onValueChangedInScroll < on: valueChanged in: scroll > value := scroll value ! ! !WidgetTest methodsFor: 'initialize' stamp: 'tak 5/8/2005 09:59'! initialize super initialize. value := 50. self define: #input as: CInputField new. self define: #scroll as: CScrollBar new! ! !WidgetTest methodsFor: 'initialize' stamp: 'tak 5/7/2005 14:23'! setupCostume super setupCostume. self layout: CTableLayout new. self hResizing: #shrinkWrap; vResizing: #shrinkWrap. input extent: 100 @ 10. input autoAccept: false. scroll extent: 100 @ 10. self add: input. self add: scroll. self signal: #valueChanged.! !