|
|
目次 |
|
|
目次 |
|
|
趣旨・導入 |
DLLCC(Dynamic-Link Library and C Connect)という仕組みをご存知でしょうか。 SmalltalkからCの、はたまたCからSmalltalkの機能の呼び出し(援用)を実現する仕組みです。
私(かめきち)が執り行っている研究で、Smalltalkからshellを動かしたかったのですが、 これをSmalltalk→C→shellといった具合に、Cを経由する形で実現すれば良いと考え至りましたので、急遽、この手に詳しい方(今回の演者:浜崎さん)に解説を乞い願ったのでした。 (つまり俺得勉強会なわけです、本当にありがとうございました!!)
ほげほげ
VisualWorksの仮想マシンはCを中心に構成されているので、原始的機能(プリミティブな機能)を独自に追加するとなれば、仮想マシンのソースコードに改変を加えて「独自の仮想マシン」を構築すれば良いのです。 しかし、ちょっとした機能追加のためだけに仮想マシンをビルドしなおすというのも難な話。もっと手軽にできないものか...という要請により生まれたのが、今回の「DLLCC」なのだとか。
OpenGLやQuickTimeのようなマルティメディアの機能を利用する際、このDLLCCは活用されています。Smalltalk→C→{OpenGL、QuickTime}という流れですね。
|
|
準備 |
最初に、ClickEarth.stをダウンロードして適当な場所に保存してください。このファイルの中には、Smalltalkのプログラムが格納されています。 今回のテーマ「地球をクリック」のプログラムの大部分は、このファイルを読み込むだけで出来上がるのですが、決して「せこい」とは言わないでくださいね... (大切なところに集中していただくための措置ですので...)
次にSmalltalkのブラウザ(リファクタリングブラウザ)を立ち上げましょう。 勉強会で配布されているVisualWorks with Junを使っている場合は「KSU」というバンドルが既に定義されており、中には2つのパッケージが含まれているはずです。 (定義されていない場合は、次に示すプログラムのうち、fileInBlockのみ実行してください。「KSU-Template」という名称のパッケージが新たに追加されるはずです。)
そのブラウザを開いたまま、新しいワークスペースを立ち上げ、以下のプログラム断片をコピー&ペーストして実行(Do it)しましょう[2]。
| fileInBlock packageBlock |
fileInBlock :=
[
[| aFilename |
aFilename := JunFileRequesterDialog requestFilename.
aFilename ifNil: [^nil].
aFilename fileIn]
on: UserInterrupt
do: [:anException | anException proceed]].
packageBlock :=
[| aCollection |
(aCollection := OrderedCollection new)
add: #comment: -> 'Copyright 2008-2012 KSU (Kyoto Sangyo University). All Right Reserved.';
add: #bundle: -> #KSU;
add: #package: -> 'KSU-Template';
add: #nameSpace: -> #KSU;
add: #category: -> 'KSU-Template';
yourself.
JunSystem
perform: ((aCollection collect: [:each | each key]) inject: String new
into: [:selector :key | selector , key]) asSymbol
withArguments: (aCollection collect: [:each | each value]) asArray].
fileInBlock value.
packageBlock value
ファイル選択ダイアログが表示されますので、先ほどダウンロードしたClickEarth.stを選択して「Accept」を押します。 では、開いていたブラウザをもう一度確認して、バンドル「KSU」を展開してみてください。新しいパッケージ「KSU-Template」が出来上がっているはずです。 さらに「KSU-Template」を選択すると「ClickEarth」というクラスが見えますね。
クラス「ClickEarth」のカテゴリ「examples」内に、クラスメソッド「example1」がありますので、実行してみましょう。 コメント内を選択して実行しても良いですし、以下の1行をワークスペースなどに貼り付けて実行するのも良いでしょう。
KSU.ClickEarth example1
真っ白なウィンドウが出てきてしまいましたが、それで良いのです。これから、この左側のウィンドウを作り込んでゆくことになります。 この2つのウィンドウを確認すれば、それらを一度閉じていただいて構いません。
さあ、これで準備は完了です。次のステップへ進みましょう。
[2] このプログラムは、ダイアログで指定したファイルのデータを読み込み、さらに「KSU-Template」というパッケージを定義するものです。
|
|
GUIの部品たち |
ようやく本題に入ります。今日のテーマ「地球をクリック」では、以下のようなGUI部品を用います。
これらGUI部品を、先ほどの真っ白な画面に貼り付けてゆきます。そう、文字通りに「貼り付け」るのです。 クラスタブのカテゴリ「interface specs」にある「windowSpec」を選択し、ブラウザの左中央にある「Edit」ボタンを押します。 すると、ウィンドウの編集画面が現れるはずです。
Menu Bar
メニューバーの取り付けは非常に簡単です。Menu Barのチェックボックスにチェックを入れるだけです。ちょうど次の画像のようになります。
チェックボックスの右側に「#menuBar」と入力されている部分がありますが、これはウィンドウが立ち上がった時に投げられるクラスメッセージ(シンボル)です。
クラス「ClickEarth」のクラスメッセージ「menuBar」を見るとわかりますが、既にメニュー一式が用意されていますよね。先の「#menuBar」で、このメニュー一式を参照しているのです。
さて、設定を変更した際には、必ず「Apply」ボタンを押して反映させておきましょう。
Click Widget
本日の主役級GUI部品「クリックウィジェット」です。
部品のボタンを押した状態で白いウィンドウにカーソルを載せてください。小さな枠が現れるはずです。その枠が見える状態ならどこでも構いませんので、一度クリックして枠を固定してください。
また、各項目を次の通りに変更してください。(「Apply」を忘れずに!)
「ID」は、その部品に付ける名前で、後ほど、このIDを使って部品を呼び出すことになります。
「Visual」は、表示物を得るためのメッセージ名を指定します。 ここでは「#imageOfEarth」としていますが、自分自身にこのメッセージを投げると2次元の地図画像を得ることができ、その画像をウィジェット内に表示しているのです。
「Default Click」は、クリックイベントが発生した時に投げるべきメッセージ名です。ユーザがウィジェット内をクリックした際に、このメッセージが呼び出されます。 末尾のコロン(:)を忘れないように気を付けてください。
Positionの設定ですが、要約すれば「512x256のサイズにする」ということです。そのパラメータが何を指しているのかが気になる場合は、実際に自分で弄ってみるのが良いでしょう。
Label
文字列を貼り付けるためのものです。
経度・緯度の2つのラベルが必要になるので、2つのラベルを貼り付けておきましょう。
経度のラベルは、Stringを「経度:」、IDを「#longitude」とします。もう一方の緯度のラベルは、Stringを「緯度:」、IDを「#latitude」とします。
InputField
文字列の入力欄です。今回は入力の機能は使わず、変化する数値を見る欄でしかありません。
これも、経度・緯度の2つ分を用意しておきましょう。
経度のフィールドのAspectもIDも「#longitudeField」とし、緯度のフィールドもAspectとIDを「#latitudeField」としておきます。 IDは単なる名称で、フィールドに対して命名しているだけです。Aspectには、そのフィールドに対応するモデルを得るためのメッセージ(シンボル)を記入します。(具体的には、ValueHolderがそのモデルに値します。)
せっかくなので背景色も変えておきましょう。
ここまで様々に貼り付けてきましたが、これらの変更を保存しておかねばなりません。 「Edit」から「Install」を選択すると、保存先を尋ねられますが、そのまま変更を加えず「OK」を押してください。これで編集完了です。 3つのウィンドウは閉じていただいて構いません。
|
|
完成 |
|
|
ドラッグ風操作への対応【発展】 |
地球自体を直接ドラッグすれば、その操作がリアルタイムに反映されて地球がクルクル回転する。なのに、平面の世界地図をドラッグしても、押した指を離さない限り地球はピクリとも動かない ── この不自然さに不満を抱いたので、今から始める機能拡張で解消したいと思います。
JavaでGUIプログラミングに取り組んだ経験をお持ちの方なら、mouseDraggedと言えば通じるでしょうか。ドラッグ操作中に繰り返し呼び出されるメソッドです。 今回の機能拡張でも、このmouseDraggedに相当するメソッドの中で、カーソル位置に合わせた地球座標変換を行えば良いだろう、と心づもりをしていたわけですが、 よくよく考えてみると今回の題材はクリックウィジェット。そう、クリック操作にしか反応しないのです。 (今回の場合、ボタンを離した時にようやく「clicked:」が呼び出されるので、ドラッグしている間は何のメッセージも投げられない...つまりドラッグを感知できないのです。)
「ClickWidget」自体を何とかして「DragWidget」でも拵えるか...と考えてみたものの、特研90分という短時間で発表できる気もせず、諦めモードに入っておりました。そんな時にひらめきが...
例えば、altキーを押しながら平面地図上をクリックして、そのaltキーを押している間、リアルタイム反映するループ処理を行えば良いじゃない!
上手く言葉で表現できないのですが、本当の「ドラッグ操作」ではない「ドラッグ風操作」になりますのが、ご了承ください...。 とかく今から実装してゆきますので、その中でご説明できればと思います。特に、プログラムの発生の過程を意識しながら進めてゆこうと思います。
クリック操作は「ボタンを押す行為」と「ボタンを離す行為」の連続によって成り立ちますが、この後者「ボタンを離す」際に投げられるのが、メッセージ「clicked:」です。 今回の拡張で変更する部分は、この「clicked:」のみになります。さあ、その変遷を追ってゆきましょう。
1. 変更前
まず変更前の「clicked:」を見ておきます。
clicked: aPoint
(self pictureOfEarth bounds containsPoint: aPoint) ifFalse: [^nil].
self
updateLongitudeField: aPoint;
updateLatitudeField: aPoint;
updateViewfinderOfEarth: aPoint
クリックされた位置の情報を基にして、経度情報・緯度情報・地球の角度を再調整します。
(self pictureOfEarth bounds containsPoint: aPoint) ifFalse: [^nil].
「self pictureOfEarth bounds」で、平面世界地図の矩形領域を得ることができますが、その矩形に対して「containsPoint: aPoint」と送って「aPointが矩形領域内にあるかい?」と尋ねているのです。 もし「否」と応えようものなら(つまり範囲外をクリックした時には)即座にnilを応答して、以降の処理は行いません。
2. altキーの押下状態を取得
今回の機能拡張ではaltキー(optionキー)を用いることとします。 そのaltキーの押下状態を知っているであろう、ClickWidgetのセンサー(キー入力を監視するもの)を連れてきて、押されているか否かを尋ねてみましょう。
clicked: aPoint
| aSensor |
(self pictureOfEarth bounds containsPoint: aPoint) ifFalse: [^nil].
self
updateLongitudeField: aPoint;
updateLatitudeField: aPoint;
updateViewfinderOfEarth: aPoint.
aSensor := (self controllerAt: #imageOfEarth) sensor.
aSensor altDown inspect
左の「False」は、altキーを押していない時の結果で、右の「True」は押している時の結果です。確かに押下状態を取得できましたね。
3. altキーが押されている間の座標を取得
altキー(optionキー)の押下で回り続けるようにして、カーソルが位置する座標を連続的に出力してみます。出力先はTranscript(トランスクリプト)になります。
clicked: aPoint
| aSensor |
(self pictureOfEarth bounds containsPoint: aPoint) ifFalse: [^nil].
self
updateLongitudeField: aPoint;
updateLatitudeField: aPoint;
updateViewfinderOfEarth: aPoint.
aSensor := (self controllerAt: #imageOfEarth) sensor.
[aSensor altDown] whileTrue:
[Processor yield.
Transcript
cr;
show: aSensor cursorPoint printString]
リアルタイムにカーソルの座標を取得できていることが確認できました。さあ、あとは経度・緯度情報と3D地球の表示を更新するだけです。
4. ドラッグ風操作に対応
ループ処理の中で得られる新しい座標を使って表示を更新すれば良いので、
clicked: aPoint
| aSensor |
(self pictureOfEarth bounds containsPoint: aPoint) ifFalse: [^nil].
self
updateLongitudeField: aPoint;
updateLatitudeField: aPoint;
updateViewfinderOfEarth: aPoint.
aSensor := (self controllerAt: #imageOfEarth) sensor.
[aSensor altDown] whileTrue:
[| thePoint |
Processor yield.
thePoint := aSensor cursorPoint.
(self pictureOfEarth bounds containsPoint: thePoint) ifFalse: [^nil].
self
updateLongitudeField: thePoint;
updateLatitudeField: thePoint;
updateViewfinderOfEarth: thePoint]
...としてやれば動くことには動きます。しかし、プログラムの重複している部分が多く、あまり良い感じがしません。ですので、ブロッククロージャにしてしまいます。
clicked: aPoint
| aBlock aSensor |
aBlock :=
[:thePoint |
(self pictureOfEarth bounds containsPoint: thePoint) ifFalse: [^nil].
self
updateLongitudeField: thePoint;
updateLatitudeField: thePoint;
updateViewfinderOfEarth: thePoint].
aBlock value: aPoint.
aSensor := (self controllerAt: #imageOfEarth) sensor.
[aSensor altDown] whileTrue:
[Processor yield.
aBlock value: aSensor cursorPoint]
重複もなく良い感じになったはずです。さて、ちゃんと動くでしょうか。(altキーを押しながら一度クリックして、altキーを離さないままマウスカーソルを移動させると動くはず。これがドラッグ風操作です。)
5. ゼロ割り回避
ドラッグ風操作をしながら平面地図の上端にカーソルを合わせてみてください。下のようなダイアログが出ることでしょう。
詳しい数学的な話は割愛しますが、とかく、プログラムのどこかで「0による除算」をしてしまったため怒られたのです。 例えば日本をクリックした時、3D地球儀の「上」とは北極を指していますので、北極を「上」としながら日本を表示すれば良いのです。 しかしながら、地図の上端(つまり北極)を見ようとすると、どこが「上」に相当するのかわかりませんよね。 今回のゼロ割りは、おそらくこういった部分が関連しているようです。
さて、このまま放置するわけにも行きませんので、次のような対策を施します。
thePoint y = 0 ifTrue: [thePoint y: JunGeometry accuracy].
渡された座標のY成分が0ならば「0ではないが、限りなく0に近い数値」に置き換えるのです。 「JunGeometry accuracy」の部分が「0に近い数値」になります。(実際にInspectしてみると「1.0d-12」が応答されます。) これを用いた「clicked:」は以下のようになります。
clicked: aPoint
| aBlock aSensor |
aBlock :=
[:thePoint |
thePoint y = 0 ifTrue: [thePoint y: JunGeometry accuracy].
(self pictureOfEarth bounds containsPoint: thePoint) ifFalse: [^nil].
self
updateLongitudeField: thePoint;
updateLatitudeField: thePoint;
updateViewfinderOfEarth: thePoint].
aBlock value: aPoint.
aSensor := (self controllerAt: #imageOfEarth) sensor.
[aSensor altDown] whileTrue:
[Processor yield.
aBlock value: aSensor cursorPoint]
example1を動かしてみて、地図の上端を選択してもエラーが出ないことを確認いただけると思います。
これで機能拡張は(一旦)終わりですが、さらなる機能拡張を志して楽しんでくださいね。日々プログラミング!
|
|
勉強会での発表を終えて【追記】 |
緊張と不安と準備不足が相まった、ツッコミどころ満載の発表となってしまい、誠に申し訳ありませんでした。。。
しかしながらその分、勉強・成長した点が沢山ありましたので、個人的な気持ちですが大変に満足しております。またお話する機会がありましたらどうぞ宜しくお願いします!
さて、発表の際に新しく会得したことを残しておきたいと思います。「clicked:」の新バージョンも拵えることができましたので。
1. 見落としていた新しい「clicked:」
この「地球をクリック」というテーマは冒頭で紹介したサイトの引用ですが、 私が実際に取り組んだ頃のデータから変更があったようで。それをろくに確認もせずに「データは以前のものと同じもの」と決めつけてかかって失敗することになります。 その「新しく提供されたclicked:」は以下のようなものです。 実は、前項で取り組んだドラッグ風操作への対応【発展】と同じような構造になっていたのです。
clicked: thePoint
| aPoint aWrapper aSensor aBoolean |
aPoint := thePoint.
(aWrapper := self builder
ifNil: [^nil]
ifNotNil: [:aBuilder | aBuilder componentAt: #imageOfEarth]) ifNil: [^nil].
aSensor := aWrapper widget controller sensor.
JunCursors crossCursor showWhile:
[aBoolean := true.
[aBoolean] whileTrue:
[aPoint := aSensor cursorPoint.
aPoint y = 0 ifTrue: [aPoint := aPoint x @ JunGeometry accuracy].
(self pictureOfEarth bounds containsPoint: aPoint)
ifTrue:
[self
updateLongitudeField: aPoint;
updateLatitudeField: aPoint;
updateViewfinderOfEarth: aPoint.
aBoolean := aSensor shiftDown]]]
押下するキーが「alt」ではなく「shift」になり、さらにマウスカーソルが一旦ウィンドウ外に出ても、領域内に戻ればリアルタイム更新が継続される... つまり、今回取り組んだ機能拡張よりも高性能なわけです。むむむ、悔しいじゃないですか...(笑)
2. 領域外からの復帰に対応する「clicked:」(オリジナル版からの拡張)
1.で紹介した新しい「clicked:」の機能に追いつくべく、オリジナル版に変更を加えます。
これまでのオリジナル版は、以下のように「領域外に出たら、その瞬間にnilを応答する」としていました。カーソルが枠外に出た瞬間に、問答無用で終了していたんですね。
(self pictureOfEarth bounds containsPoint: thePoint) ifFalse: [^nil].
self
updateLongitudeField: thePoint;
updateLatitudeField: thePoint;
updateViewfinderOfEarth: thePoint].
そうではなく、逆に「領域内にいる時は更新処理をして、領域外にいる時はどうでも良いよ〜」とします。つまり、領域外に出ても終了しなければ良いのです。
(self pictureOfEarth bounds containsPoint: thePoint)
ifTrue:
[self
updateLongitudeField: thePoint;
updateLatitudeField: thePoint;
updateViewfinderOfEarth: thePoint].
これを反映したのが以下の「clicked:」です。
clicked: aPoint
| aBlock aSensor |
aBlock :=
[:thePoint |
thePoint y = 0 ifTrue: [thePoint y: JunGeometry accuracy].
(self pictureOfEarth bounds containsPoint: thePoint)
ifTrue:
[self
updateLongitudeField: thePoint;
updateLatitudeField: thePoint;
updateViewfinderOfEarth: thePoint]].
aBlock value: aPoint.
aSensor := (self controllerAt: #imageOfEarth) sensor.
[aSensor altDown] whileTrue:
[Processor yield.
aBlock value: aSensor cursorPoint]
これで良い感じになりましたね! まだ改良の余地はあると思いますが、それはまた気が向いた時に...(
3. Menuさんには「startUp」
menuBarの在処に関するツッコミを受けた際に出てきた事項。menuBarのソースコードを見てみると、
menuBar
"Tools.MenuEditor new openOnClass: self andSelector: #menuBar"
<resource: #menu>
^#(#{UI.Menu} #(
#(#{UI.MenuItem}
#rawLabel: 'ファイル'
#submenu: #(#{UI.Menu} #(
#(#{UI.MenuItem}
#rawLabel: '地球'
#value: #viewEarth )
#(#{UI.MenuItem}
#rawLabel: '終了'
#value: #closeRequest ) ) #(1 1 ) nil ) ) ) #(1 ) nil ) decodeAsLiteralArray
...という具合になっている。とかくシンボルだらけのプログラムですが、「^」を除いた部分を選択してInspectしてみてください。実はこれでMenuのインスタンスを得ることができるのです。
さらに、そのMenuのインスタンスに対して「startUp」を送ってみてください。その場でメニューが開かれるのです!
さすがSmalltalkです、「オブジェクトに話しかける」という感覚をヒシヒシと実感することができますね。
4. VisualLauncher open
発表の最中、私の誤操作でトランスクリプトのウィンドウを消失してしまいまして、その際に青木さんよりご指南いただいたのが、この「VisualLauncher open」。
これでトランスクリプトを開けることができるのです。またこれは複数枚開けることも可能なので...
...という具合にもできたりします。(特に意味は無いですけどね...)
|
|
重要そうなプログラム部位【資料】 |
発表の中で特に触れておいたほうが良いかなと感じた部分を列挙しておきます。
ウィンドウを開いた直後に評価されるメッセージ。平面地図と地球を出して、地図の中央を押したことにせよ、とのこと。
postOpenWith: aBuilder
super postOpenWith: aBuilder.
self
pictureOfEarth;
viewEarth;
clicked: self pictureOfEarth bounds center
地球を表示するための下準備。
pictureOfEarth
pictureOfEarth
ifNil:
[self builder
ifNotNil:
[:aBuilder |
(aBuilder componentAt: #imageOfEarth)
ifNotNil:
[:aWrapper |
aWrapper widget
ifNotNil: [:aView | pictureOfEarth := aView visual]]].
pictureOfEarth ifNil: [pictureOfEarth := self class imageOfEarth]].
^pictureOfEarth
経度の更新処理。これがわかれば緯度もほぼ同じ。この辺の符号を変えてやると、地球が逆さまになったりする...という話も触れたいですね。南半球の人向けのプログラムだって作ることができます。
updateLongitudeField: aPoint
| anImage aStream halfWidth aValue aString |
anImage := self pictureOfEarth.
aStream := String new writeStream.
halfWidth := anImage width / 2.
aValue := aPoint x.
aValue <= halfWidth
ifTrue:
[aValue := aValue / halfWidth * 180.
aStream nextPutAll: '東経']
ifFalse:
[aValue := (halfWidth - (aValue - halfWidth)) / halfWidth * 180.
aStream nextPutAll: '西経'].
aStream
nextPutAll: aValue asInteger printString;
nextPutAll: '度';
nextPutAll: ((aValue - aValue asInteger) * 60) asInteger printString;
nextPutAll: '分'.
aString := aStream contents.
self longitudeField value: aString
3次元グラフィックス。視点の変換。特研でも頻出でした。
updateViewfinderOfEarth: aPoint
| aViewfinder anImage longitudeValue latitudeValue aBlock eyeBeam projectionTable |
(aViewfinder := self viewfinderOfEarth) ifNil: [^nil].
anImage := self pictureOfEarth.
longitudeValue := aPoint x / anImage width * 360.
latitudeValue := aPoint y / anImage height * 180.
aBlock :=
[:longitude :latitude |
| aTransformation |
aTransformation := Jun3dTransformation unity.
aTransformation := aTransformation
product: (Jun3dTransformation rotateY: (JunAngle degrees: latitude)).
aTransformation := aTransformation
product: (Jun3dTransformation rotateZ: (JunAngle degrees: longitude)).
aTransformation yourself].
eyeBeam := (0 , 0 , 0
to: 0 , 0 , (aViewfinder eyePoint distance: aViewfinder sightPoint))
transform: (aBlock value: longitudeValue value: latitudeValue).
projectionTable := aViewfinder projectionTable.
projectionTable
at: #sightPoint put: eyeBeam first;
at: #eyePoint put: eyeBeam last;
at: #upVector put: 0 , 0 , 1.
aViewfinder projectionTable: projectionTable
開け〜(ウィンドウ)
viewEarth
self viewfinderOfEarth
ifNotNil:
[:aViewfinder |
| aBuilder aWindow |
(((aBuilder := aViewfinder builder) notNil
and: [(aWindow := aBuilder window) notNil])
and: [aWindow isOpen])
ifTrue:
[aWindow isCollapsed ifTrue: [aWindow expand].
aWindow raise]
ifFalse:
[| aWrapper aView |
(((aBuilder := self builder) notNil
and: [(aWrapper := aBuilder componentAt: #imageOfEarth) notNil])
and: [(aView := aWrapper widget) notNil])
ifTrue:
[| aBox aRectangle |
aBox := aView topComponent displayBox.
aRectangle := aViewfinder class windowBounds.
aRectangle := aRectangle align: aRectangle bottomLeft with: aBox bottomRight.
aViewfinder openAt: aRectangle origin]
ifFalse: [aViewfinder open]]]
地球の正体。
imageOfEarth
^JunOpenGLTexture imageEarth3