何某日和

「カメラ」のち「ハンダゴテ」ところにより「プログラミング」 ── そんな私“かめきち”のウェブサイト

2012.10.03:テキストエディタ

概要

 前回の資料には力を入れすぎましたが、今回からは本当に概要のみまとめておきます。(じゃないと研究がおろそかに!orz) 今回はGUIシリーズ第1弾の「テキストエディタ」の作成に取り組みます。 ValueHolderと呼ばれるものの役割を追いながら、 ペアプログラミングを通して、GUI部品の取り扱い方・メニューの作り方・ファイル入出力などの基礎を会得していただきます。 ちなみにテキストはこちら(http://www.cc.kyoto-su.ac.jp/~atsushi/Programs/TextEditor/index-j.html

準備

マシンメンテナンス

 ペアプログラミングの冒頭から脱線しますが、まずはマシンメンテナンスを行いましょう。職人道具は日々の手入れが欠かせないものです。 こちら(日々のマシンメンテナンス)を参考に定期励行を心がけてください。

準備プログラムの実行

 さて、Smalltalkプログラミングを執り行うための準備を行いますが、その準備自体をプログラムで行います。(なんだか、少しメタですね。) ワークスペースに以下のプログラム断片を貼り付け、Do itしてみてください。準備が整うはずです。

| aString aURL aFilename aCollection |
aString := 'http://www.cc.kyoto-su.ac.jp/~atsushi/Programs/loading.st'.
aURL := JunURL named: aString.
aURL exists ifFalse: [^nil].
aFilename := Filename defaultDirectory construct: aURL asURI tail.
aURL downloadTo: aFilename.
aFilename exists ifFalse: [^nil].
aFilename fileIn.
(aCollection := OrderedCollection new)
    add: #url: -> 'http://www.cc.kyoto-su.ac.jp/~atsushi/Programs/TextEditor/TextEditor.st';
    add: #comment: -> 'Copyright 2008-2011 KSU (Kyoto Sangyo University). All Right Reserved.';
    add: #bundle: -> #KSU;
    add: #package: -> 'KSU-Template';
    add: #nameSpace: -> #KSU;
    add: #category: -> 'KSU-Template';
    add: #class: -> #{KSU.TextEditor};
    add: #protocol: -> #examples;
    add: #selector: -> #example1;
    add: #execute: -> [#{KSU.TextEditor} value example1];
    yourself.
JunSystem
    perform: ((aCollection collect: [:each | each key]) inject: String new
            into: [:selector :key | selector , key]) asSymbol
    withArguments: (aCollection collect: [:each | each value]) asArray
ワークスペースでプログラムを実行 プログラムが立ち上がる

今回のGUI部品:Text Editor

  • GUI Painter(メニューを出す)
  • メニューの作成
  • GUI Painter(Text Editorの配置)
  • Position : ProportionとOffset
  • ちゃんと動きますか?

GUI Painter(メニューバーを出す)

TextEditorのinterface specsに「windowSpec」があるので、Editしましょう。

Palette:GUI部品たち GUI Painter Tool

GUI部品を提供する「Palette」と、その部品に対して様々な設定を施す「GUI Painter Tool」が表示されます。 まずは、「GUI Painter Tool」でメニューバーを有効にしましょう。チェックを入れた後に「Apply」して「Install」すれば準備完了です。 「windowSpec」をOpenして確かめましょう。メニューバーが表示されたはずです。

メニューの作成

未完成のメニュー メニュー(ファイル) メニュー(編集)

次にメニューを作りこみます。左は未完成のメニューで、中と右は完成後のメニューです。 TextEditorのresourcesに「menuBar」があるので、Editしましょう。

メニューエディタ

編集した後に「Install」すれば準備完了です。ちなみに、各ボタンと、押下時に送るメッセージは以下の通りです。

  • ファイル
    • 新規:#newFile
    • 開く:#openFile
    • 保存:#saveFile
    • 終了:#close
  • 編集
    • カット:#cut
    • コピー:#copySelection
    • ペースト:#paste
    • すべて選択:#selectAll

GUI Painter(Text Editorの配置)

TextEditor Basics

今回の主役となるGUI部品「Text Editor」を選択し、適当な位置に貼り付けてください。また「GUI Painter Tool」のBasicsタブの3項目を図の通り埋めてください。

Position : ProportionとOffset

適当に貼り付けた「Text Editor」の位置とサイズを調整します。「GUI Painter Tool」のPositionタブを開くと、「Proportion」と「Offset」という2項目があるはずです。 「Proportion」は左右位置または上下位置の割合を表し、「Offset」は周辺余白を表します。

Proportion(上下左右の割合) Offset(上下左右の余白)

今回の場合は次のように設定すると良いでしょう。「Offset」の符号に注意しましょう。

TextEditorのPosition 位置とサイズを調整した後の画面

ちゃんと動きますか?

example1を動かした例

ここまで作ると、一部機能(保存と読み込み)を除いて「テキストエディタ」が使えるようになるはずです。一度動作を確認してみてください。

コードリーディング

  • textHolder
  • ValueHolder
  • menu messages

textHolder

簡単に説明すると、文字列を作って、「asText」メッセージでテキストに変化させ、さらに「asValue」メッセージでValueHolderに変化させます。この、ValueHolderが今回のキーポイントになります。 (というよりも、これから頻出することになるでしょう。)

ValueHolder

MVCのモデルに相当する「ValueHolder」の例に少しだけ触れてみましょう。

3 asValue

ValueHolderは何らかの値を保持しておいてくれるオブジェクトです。もっと簡単に言えば変数のようなものです。 まずは、上の「3 asValue」をインスペクトして対話してみましょう。

self value

これは「あなたの持つ値は何ですか?」と訊いているのです。もちろん「3」と答えてくれます。

self value: 4

これは「あなたの持つ値は4ですよ」と言い聞かせているのです。もう、「3」ではなく「4」を保持しているのだ、ということを伝えたのです。 では、ここで「あなたの持つ値は何ですか?」と訊くと、何と答えるでしょうか...。

self value

確かに「4」と答えてくれます。とかく、保持している値は変更できるようです。では、ちょっと面白いことをしてみましょう。 保持している値が変更されたら、トランスクリプトに「Changed!!」と表示されるようにするのです。

self compute: [:aValue | Transcript show: 'Changed!!']

これを実行しても何も起こりませんが、心配いりません。ValueHolderさんに仕事を依頼しただけですから。 「もし、あなたの保持する値が変更されたら、'Changed!!'と叫んでください。」とお願いしたのです。

では、実際に値を変更してみましょう。「4」ではなく「5」を持たせましょう。

self value: 5

いかがでしょう。きっと、トランスクリプトには「Changed!!」と書かれているはずです。ValueHolderさんが、ちゃんと叫んでくれたんですね!

ValueHolderが叫んだ「Changed!!」

「もし、あなた(ValueHolder)の値が変わったら、私(View)に知らせてね!」という仕事を依頼しておいたとすれば、これはまさにMVCの構造そのものだということも理解できるでしょう。

これらは遅延評価の機構(ブロッククロージャ)があるために成し得る技です。 いつまでも逐次的な処理ばかり考えているのではなく、「何かが起こればこれをする」という「イベント駆動」のプログラミングにも慣れ親しんでみてください。 ValueHolderには、複数の仕事を依頼しておくこともできるので、もっと面白いことができるはず。 プログラミングの表現の幅もグググッと広がって、あとはあなたのアイデア次第...(余韻

menu messages

各種メニューから動作させることができるプログラムが集まっています。メッセージ名から類推できる処理を行うだけですので、特段に説明しなければならない部分はないでしょう。(「newFile」くらい?) 「openFile」と「saveFile」は次節で実装します。

コードライティング

  • openFile
  • saveFile

プログラムはまだ完成していません。「openFile」と「saveFile」をここで実装することにしましょう。(※プロマネAさんのご助力による)

インスタンスメソッド一覧

openFile

 完成版のopenFileは後ほど示すことにして、その完成版に至るまでの「プログラムの生い立ち」を見てゆきます。 ワークスペース等に貼り付けてDo itまたはInspect itしながら、発展の過程を垣間見ていただければと思います。

| aMenuBuilder aMenu |
aMenuBuilder := MenuBuilder new.
aMenuBuilder add: 'テキストファイル' -> (Array with: '*.txt' with: '*.TXT').
aMenuBuilder line.
aMenuBuilder add: 'すべてのファイル' -> (Array with: '*').
aMenu := aMenuBuilder menu.
aMenu yourself

これをInspect itするとメニューが得られるはずです。「メニュー」というほどですから、きっと「メニューを開く」こともできるはず。 それでは、「メニューを開いてください!」とお願いしてみましょう。(startUpというメッセージを送ってみてください。)

作成したメニューを開く

確かにメニューが作られたようですね。

| aMenuBuilder aMenu aFilename |
aMenuBuilder := MenuBuilder new.
aMenuBuilder add: 'テキストファイル' -> (Array with: '*.txt' with: '*.TXT').
aMenuBuilder line.
aMenuBuilder add: 'すべてのファイル' -> (Array with: '*').
aMenu := aMenuBuilder menu.
aFilename := JunFileRequesterDialog
            requestFilename: 'テキストファイルを選択してください。'
            fileTypeMenu: aMenu
            initialFileType: aMenu values first.
aFilename yourself

さっき作ったメニューをJunFileRequesterDialogに渡して何かしているようですね。これをInspect itしてみてください。 ファイル選択用のダイアログが表示されるはずです。

ファイル選択ダイアログを開く 選択されたファイル nil(ファイルは選択されなかった)

最後に応答されるのはユーザが選択したファイルです。何も選択せずに「Cancel」した場合は、nilが応答されます。ちなみに、渡したメニューはファイルの種類を選択するためのメニューだったのです。

こうして得られたファイル名(aFilename)を使って、必要なデータを読み込むのです。次の3行によって読み込みを実現します。

aStream := (aFilename withEncoding: #UTF_8) readStream.
[aString := aStream contents] ensure: [aStream close].
self textHolder value: aString

それでは、これらを結合した「openFile」を下に示します。

openFile

    | aMenuBuilder aMenu aFilename aStream aString |
    aMenuBuilder := MenuBuilder new.
    aMenuBuilder add: 'テキストファイル' -> (Array with: '*.txt' with: '*.TXT').
    aMenuBuilder line.
    aMenuBuilder add: 'すべてのファイル' -> (Array with: '*').
    aMenu := aMenuBuilder menu.
    aFilename := JunFileRequesterDialog
                requestFilename: 'テキストファイルを選択してください。'
                fileTypeMenu: aMenu
                initialFileType: aMenu values first.
    aFilename ifNil: [^nil].
    aStream := (aFilename withEncoding: #UTF_8) readStream.
    [aString := aStream contents] ensure: [aStream close].
    self textHolder value: aString

saveFile

次に、テキストデータの保存処理を行う「saveFile」を実装しましょう。「openFile」との違いはほんの少しですので、説明は省略します。

saveFile

    | aMenuBuilder aMenu aFilename aStream aString |
    aMenuBuilder := MenuBuilder new.
    aMenuBuilder add: 'テキストファイル' -> (Array with: '*.txt' with: '*.TXT').
    aMenuBuilder line.
    aMenuBuilder add: 'すべてのファイル' -> (Array with: '*').
    aMenu := aMenuBuilder menu.
    aFilename := JunFileRequesterDialog
                requestNewFilename: 'テキストファイルを入力してください。'
                initialFilename: 'zzz.txt'
                fileTypeMenu: aMenu
                initialFileType: aMenu values first.
    aFilename ifNil: [^nil].
    aString := self textHolder value.
    aStream := (aFilename withEncoding: #UTF_8) writeStream.
    [aStream nextPutAll: aString] ensure: [aStream close]

さいごに

GUIシリーズ第1弾として、初めて触れるツールも多かった(と言いつつ、きっと特研・他で扱っていた)と思いますが、 とかくGUIアプリケーションが手軽に作成できるということを垣間見ることができたでしょうか。 また、今日触れたValueHolderは、これから先も頻出です。その中で、より具体的なイメージを会得してもらえればと思います。今はまだぼんやりで構いません。