RealBasic University

このコラムはStone Table Softwareのオーナーであり、またREALbasic Developerの編集者でもあるMarc Zeedar氏により書かれたものを、著者の許可を得て翻訳したものです。この翻訳はHREM Researchにより提供されています。この日本語版へのご意見はRBU-Jまでご連絡下さい。

URL: http://www.applelinks.com/rbu/092/
INDEXに戻る

OOP University:パート 16

前回からSuperDrawの機能拡張をはじめました。今回は、インターフェースの向上と、画像の読み込み/保存の機能を追加していきましょう。

私はこれが最後のSuperDrawのレッスンになるだろうと考えていましたが、思ったよりも時間をとられてしまったので、今回で最後とはなりません。しかし、実のところRBUは一週間発表が遅れているので、今週は2つ発表して遅れを取り戻し、今回のレッスンの第2部は来週まで待つことがないようにいたします。

インターフェースの向上

まずはいちばん簡単なところから手をつけましょう。私がSimpleDrawを開始したときは、簡単のため、描画ウィンドウと同じウィンドウにプッシュボタン式の「ツールパレット」を作成しました。例題のプロジェクトとしてはこれで充分なのですが、SuperDrawのような多機能の描画プログラムとしては、ツールパレットを別ウィンドウにした方が扱いやすいでしょう。それによってツールパレットの外観も同時に改善することができます。

はじめに、新規ウィンドウを追加(ファイルメニューの「新規ウィンドウ」) して、次のようなプロパティを追加してください。

次に、アイコンのあるこのフォルダーをダウンロードしてください。この画像をインポートするには、フォルダー全体をREALbasicのプロジェクト・フォルダーにドラッグするだけです。それから、6つのbevelButtonsobjectPaletteに配置して、コントロール配列(control array)としてそれを作成します(それはすべて同じnameプロパティを持っていなければなりません)。いちばん先頭のindexプロパティはゼロ、2番目は1、3番目は2というようにします。bevelButtonでは、captionプロパティを適切なテキストに、iconを正しい画像に設定します。すべて完了すれば、それは次のようになっているでしょう。

自作の絵はあまり上手くありませんが、ないよりはましです。それに今後これをどのように拡張していけばよいかのヒントになるでしょう。あなたのSuperDrawの見た目をもっと格好よくしたいならば、どうぞご自由にチャレンジしてみてください。

このウィンドウの作成を早くすませたいけれども、あとのプロジェクトにもしっかりついて行きたい(完成版をダウンロードするというのではなく)という場合は、ここからウィンドウだけをダウンロードすることができます。

objectPaletteに追加していくコードには、難しいことはひとつもありません。window1の同じボタンに使われたコードをそのまま適応します。次のコードを、objectPaletteのaddObjectButtonActionイベントに入力してください。

  
dim l, t, w, h as integer

// サイズ・場所のランダム生成
w = round(rnd * 50) + 10
h = round(rnd * 50) + 10
l = rnd * (window1.canvas1.width - w)
t = rnd * (window1.canvas1.height - h)

window1.canvas1.addObject(index, l, t, w, h)

これは、window1のcanvas1内のサイズと位置をランダムに生成して、そのオブジェクトに、ランダム生成されたサイズと位置で、新規の描画オブジェクトを追加するよう伝えます。簡単です!

技術的に言えば、ここでもう完成――プログラムとパレットは機能するはず――ですが、外観と動作をもっと向上させるために、必要なことがあと少しあります。

はじめに、window1を開いて、そこにあるpushButtonsを削除してください。それから、canvas1を300x300の四角形に設定して、window1もそれに合わせて325x325に設定します。これで描画領域は専属のウィンドウになりました。こうすれば、canvas1の4つの固定プロパティ(lockleftlocktopなど) を固定して、window1のgrowBoxプロパティをtrueに設定することが可能です。これで、ユーザは描画領域のサイズが変更できるようになります。

最後に、window1のOpenイベントに次のコードを入力しましょう。

  
objectPalette.top = 50
objectPalette.show

me.left = objectPalette.left + objectPalette.width + 25

これは、objectPalettewindow1が正しい場所に開かれるように約束するものです。

これでいいでしょう。ここはもう充分なので、画像の保存に入りましょう!

ファイルの保存と読み込み

SuperDrawの最大の欠点は、描画したものの保存や取り出しができないことです。しかし、画像はたんなるオブジェクトのリストにすぎないので、それをファイルに保存することは簡単にできます。

標準ファイル・フォーマットの作成

ところが、我々のオブジェクト指向の方法は、新しい種類のオブジェクトや機能をプログラムに追加することができる一方で、「最終的」な描画ファイルのフォーマットは難しくなる問題があることを知っておくことが重要です。たとえば、SuperDrawが5種類のオブジェクトをサポートしていて、その5種類を保存するルーチンを作るとしたら、その保存ルーチンは6番目のオブジェクトに対応できません。同様に、読み込みのルーチンも新しいオブジェクトに対応するために修正が必要になります。

カラー設定や、他の新しいプロパティを既存のオブジェクトに追加する場合も、同様の問題が起こります。もし第三者によって使用されるプログラムを作成しているならば、ある人は古いバージョンのアプリケーションで、新しいバージョンによって生成されたファイルを開くかもしれないということを頭に入れておく必要があるでしょう(その逆の場合も同様です)。

私の保存/読み込みルーチンは、いまある基本的なオブジェクトではうまく機能しますが、あなたがオブジェクトに手を加えた場合は、そのオブジェクトに対応するためにルーチンの修正が必要になってくるでしょう。

この状況を解決しないと、プログラムはそのオブジェクトやオブジェクトの特定の特徴を失ってしまう(保存できない)か、読み込みや保存の際にクラッシュしてしまう可能性があります(たとえば、描画オブジェクトの存在しないプロパティの特徴を保存したり、読み出そうとする場合)。

この問題の回避策はいくつかあります。そのどれもSuperDrawには実装しませんが、SDのファイル・フォーマット対応をもっと改善することに興味のある方のために、いくつか方法論を簡単にご説明しましょう。

もっとも簡単な「解決策」は、すべての保存ファイルにバージョン番号を付加し、ファイル読み込みのルーチンで扱えないバージョンはすべて無視してしまうことです。つまり、ファイルのルーチンがバージョン10(あるいは任意の数)まであるとしたら、バージョンが「10」であるファイルしか開きません。ファイル読み込みのルーチンは、フォーマットの種類が明らかでないので、それ以外のバージョン番号のものは開きません。言うまでもなく、異なるバージョンのファイルを扱うエンド・ユーザにとっては、これはあまりにも不便です。

これを少しだけ改善させたものは、サポートするファイル・バージョンのひとつひとつに対応する複数の「読み込み」ルーチンをプログラムに含ませることです。そうして、検知されたバージョンにもとづいて、適切な読み込み方法を呼び出します。これにはより多くの作業を要しますが、エンド・ユーザにとっては見通しのよい方法です。

もっと強力な方法は、動的で「無制限」のオブジェクト指向型ファイル・フォーマットを作成することです。これはおそろしく複雑ですが、基本的な方法としては、ファイル・フォーマットに必要十分な情報を含めることです。プログラムの読み込みルーチンはそれを認識して適切なオブジェクトを読み出します。たとえば、オブジェクトが整数のプロパティを持つとしましょう。ファイル・フォーマットがそれは整数のプロパティで値は何々だと知らせてくれれば、読み込みルーチンは設定しているプロパティも同様に整数のプロパティであるか確認することができます(したがって、プロパティに異なるデータ型を割り当てるという、クラッシュの原因となる動作をしないよう簡単なエラー・チェックを行うことになります)。

当然ですが、未知のオブジェクトをどのように扱うかを教えてくれるファイル・フォーマットなどありません。しかし、付加情報を含めることで、プログラムの認識率が高くなり、対応しているオブジェクトは読み込み、未知のオブジェクトは無視することができるようになります。ユーザにとって見れば、新しいバージョンで保存した画像を古いバージョンのプログラムで開いても、クラッシュやエラーの原因になりませんが、読み込みに限っては不完全な画像となります――しかし、読み出せないよりはましでしょう。当然ですが、このときはユーザに警告しておくのが親切です。

よく売れている商用品では、このテクニックを組み合わせて使ってます。たとえば、Adobe Illustratorは、すべての要素が保存できない古いバージョンのフォーマットで画像を保存(「Save As」コマンドで)しようとすると、警告を発します。そして、いくつかのオブジェクトや要素が失われる可能性がありますと警告されますが、古いバージョンのプログラムで新しいIllustratorファイルを開くこともできます。それをどう処理すればよいかアイデアを得るため、他のプログラムではこの問題にどのように対応しているのかよく注意して見ましょう。

メニューの追加

ファイルの保存に対応するためには、メニュー・コマンドがさらに必要ですので、プロジェクト・ファイルでMenuオブジェクトを開き、ファイルメニューでFileNewFileOpen、そしてFileSaveを追加してください。それらのCommandKeyプロパティは、それぞれNOSと設定します。

Window1のenableMenuItemsイベントに、次のコードを入力します。

  
if canvas1.objectCount > 0 then
editSelectAll.enabled = true
fileSave.enabled = true
end if
fileNew.enabled = true
fileOpen.enabled = true

それから、3つの新しいメニュー・ハンドラー(編集メニューの「新規メニューハンドラー」)を追加してください。FileNewには次を入力してください。

  
canvas1.eraseAllObjects

次は、FileOpenのコードとなります。

  
canvas1.loadFile(getOpenFolderItem("superdraw"))

そして、FileSaveは次のようになります。

  
canvas1.saveFile

ご覧の通り、ここで行っているのはメッセージをcanvas1に渡し、ファイルを開くか保存するかをそれに伝えます。

最後に、FileOpenルーチンで「superdraw」と呼ばれるファイル・タイプを使っていることに気がついたと思います―― それをプロジェクトに定義しなければなりません。編集メニューの「File Types...」に行き、次の設定で新しく追加してください。

オッケーです。これでコードを書く準備は整いました!

保存、読み込みメソッド

まずはdrawCanvasClassを開きましょう。ファイルの読み込みと保存を行うメソッドを追加するのはこの描画キャンバスです。

eraseAllObjectsと呼ぶメソッドを最初に追加します(編集メニューの「新規メソッド」)。次はそのコードです。

  
redim objectList(0)
me.refresh

ご覧のように、これは単に現在の画像を消去するだけです。リフレッシュ(refresh)コマンドは、ウィンドウを再描画します。このコマンドは、ユーザが新しいファイルを作成したとき(メニューコマンドの「新規」によって)、また新規ファイルが開かれたとき(現在の画像が消去されます)のふたつの異なる状況で使用されます。

いまのところ、SuperDrawはユーザに現在の画像が消去されることについての警告はしてくれませんので、それは一瞬のうちに消去されてしまいます。いうまでもなく、商用のプログラムでは、警告は欠かすことのできない機能となります。

この機能を追加するもっともよい方法は、eraseExistingDrawingという論理型(boolean)を返り値として持つ新規イベントをdrawCanvasClassに追加するとことです。返り値がfalse(デフォルト)であれば、消去が続けられ、それ以外では停止します。eraseAllObjectsを次のように変更しましょう。

  
if not eraseExistingDrawing then
redim objectList(0)
me.refresh
end if

その後は、消去についてユーザに確認をするコードをeraseExistingDrawingイベントのcanvas1に追加するかは自由です。FileNewに関しては、キャンセルしてもその結果には何も影響がありません。しかしFileOpenルーチンでは、全オブジェクトの消去が中止されれば、ファイルを開く動作はキャンセルされなければなりません。

いいですか、では保存に移りましょう。最初に、前回に保存されたファイル名を記憶しておきたいので(描画が保存されるたびに、ファイル名をユーザに聞きたくありません)、ファイルを記憶しておくためのプロパティをdrawCanvasClassに追加する必要があります。このプロパティをtheDocument as folderItemとして追加(編集メニューの「新規プロパティ」)してください。

次は、saveFileメソッドです。我々のファイル・フォーマットはとてもシンプルであることを心に留めておきましょう。保存ファイルのいちばん最初は、保存するオブジェクトがいくつであるかを指示する整数です。それから、データ構造のオブジェクトそれぞれをループして、オブジェクトの種類を示す整数を保存し、その後にlefttopwidthheight、そしてlineSizeプロパティ(これらはshapeClassのプロパティなので、すべてのオブジェクトに共通です)が続きます。そして、オブジェクトが選択されたかどうかを示す偶数(0)か奇数(1)の値を保存します。最後に、オブジェクトの種類に依存する情報を保存します。たとえば、テキストオブジェクトでは、使用されたフォントと、特定されたテキスト情報を保存します。

 sub saveFile() 
dim binFile as binaryStream
dim i, n, kind as integer
dim o as shapeClass
dim s as string

if theDocument = nil then
theDocument = getSaveFolderItem("superdraw", "untitled drawing.sd")
end if

if theDocument <> nil then
binFile = theDocument.createBinaryFile("superdraw")
if binFile <> nil then
o = new shapeClass
n = objectCount
binFile.WriteLong n

for i = 1 to n
o = objectList(i)
kind = -1
if o isa circleClass then
kind = 0
elseif o isa rectClass then
kind = 1
elseif o isa triangleClass then
kind = 2
elseif o isa polygonClass then
kind = 3
elseif o isa pictClass then
kind = 4
elseif o isa textClass then
kind = 5
end if

// Save object kind
binFile.writeLong kind
// Save standard shapeClass properties (common to all subclasses)
binFile.writeLong o.left
binFile.writeLong o.top
binFile.writeLong o.width
binFile.writeLong o.height
binFile.writeLong o.getLineSize
if o.selected then
binFile.writeByte 1
else
binFile.writeByte 0
end if

select case kind
case 0 // Circle
// No additional properties
case 1 // Rectangle
// No additional properties
case 2 // Triangle
// No additional properties
case 3 // Polygon
// No additional properties
case 4 // Picture
s = getPictureData(pictClass(o).image)
binFile.writeLong len(s)
binFile.write s
case 5 // Textblock
binFile.writeLong len(textClass(o).text)
binFile.write textClass(o).text
binFile.writeLong len(textClass(o).textFont)
binFile.write textClass(o).textFont
end select
next // i

else
beep
msgBox "Sorry, could not create the file."
end if // binFile = nil
end if // theDocument = nil
end sub

あなたがbinaryStreamファイルのオブジェクトにそれほど慣れていないならば、これはこの種類のデータに対して理想的です。保存されるそれぞれのタイプの情報は、標準サイズ(バイト)です。たとえば、writeLongメソッドは4バイトの情報を書き込みます。それと対称のコマンドであるreadLongを使うと、それは常に4バイトの情報を読み込みます。保存と読み込みのルーチンがデータ型で一致している限りは、ファイルは正常に読むことができます。しかし、そこで不一致が起こるようなこと――longで読み書きするところを、byteで読み書きしてしまう――があれば、ファイルには問題が発生します。この種類のファイル・フォーマットには、エラーは許されず、きわめて高い正確さを必要とします。コンピュータはこのような反復作業では威力を発揮し、不満も言わずに何度も何度も完璧に行います。しかし、読み書きのルーチンを正確に書いて、完全にバランスのとれた状態にしなければ、機能しません。

さて、上記のcase 5部分を検討してみましょう。ここでは、どのようなデータでも簡単に書き込めるbinaryStreamのwriteコマンドを活用しています。この種類のデータには決まった長さがありませんので、このデータの長さを書き込む前に保存して、あとで読み込みルーチンがどれだけ読み込めばよいかわかるようにしなければなりません。

それで、最初にすることはテキストの長さ(バイト)を表すlongを書き込みます。それからテキストを書き込みます。これをフォントの名前の長さとフォントの名前そのものに対しても繰り返し行います。ファイルの読み込みルーチンでは、これとは逆のことを行います。つまりlongで読み込みてその値を保存し、どれだけの情報を読み込めばよいかbinaryStreamに伝えるためにそれを使用します。

詳細

このルーチンでは、とても重要なREALbasicコマンドであるisaを活用したことに気がつきましたか。

  
if o isa circleClass then
...

このオペレータは、どのような種類のオブジェクトを扱っているかを調べてくれるので、我々はkind変数を設定することができます。これは他に類を見ないくらいに強力で重要なものです。これなしでは(少なくともその種類を伝えるオブジェクトの方法論を作成しなければ)、どの種類のオブジェクトを現在扱っているのか知ることができないため、このオペレータは重要なものとなります。

しかし同時に、オブジェクトのサブクラス化の機能によって、それは強力なものとなります。circleClassは、shapeClassのサブクラスであることを覚えておきましょう。そのため、circleClassshapeClassは、ほとんど互換的に使用できます。ほとんどと言ったのは、オブジェクトは明らかに異なるので充分な注意が必要になるからです。例えば、アクセスしようとしているオブジェクトが実際にはshapeClassrectClassのようなときに、circleClassの独自のプロパティにアクセスしようとするとクラッシュしてしまいます。

しかし、それは同じペアレントを共有しているので、メソッドの設定ではパラメータとしてshapeClassを指定して、実際に渡すときには、circleClassrectClass、あるいはshapeClassのその他のサブクラスを指定することができます。なぜならば、ある意味、circleClassshapeClassだからです!

  
sub aMethod(theObject as shapeClass)
if theObject isa circleClass then
...

重要事項:これは、子から親へのような反対方向の場合にはうまくいきません。パラメータ型としてcircleClassを受け付けるためにaMethodを設定した場合、それをshapeClassに送ることはできません。なぜならばshapeClasscircleClassではないからです。circleClassshapeClassから継承するので、それはshapeClassですが、反対の場合は成り立ちません。

こうすることで1つの一般化されたメソッドで異なる種類のオブジェクトを処理できるため、これを使用することはとても強力です。それをisaと組み合わせて使用することで、メソッドはカスタム化されたサブオブジェクトを処理することができます。

コードの大きな簡略化につながるので、この方法はぜひ覚えておきましょう。特定のオブジェクトのみに対して機能するルーチンよりも、一般的なオブジェクトに対して機能するルーチンを作成することは、より良いことです。

これはサブクラス化されたあらゆる種類のオブジェクトに対して適応できます。ですから、パラメータとしてeditFieldを必要とするルーチンを作成して、実際には自分の作ったeditFieldのサブクラスを渡すことができます。

仕上げ

残念ですが、今回は時間がなくなってしまいました――このコラムは通常よりも長かったのですが、ファイルの保存・読み込みルーチンの難しいところを説明するまで行けませんでした!

いちばんの問題はpictClassオブジェクトです。我々は実際の画像データをファイルに保存しなければなりませんが、REALbasicにはそのデータにアクセスする簡単な方法がありません。よって、いくつかのステップを踏んで、その問題を回避していこうと思います。試してみればわかるのですが、現在のコードは実行できません――まだいくつかの必要とされる重要な機能を書いていないのです。次回のレッスンで、すべて説明したいと思います。

それまでは、必要なものがすべてそろっているSuperDrawの完全なプロジェクト・ファイルを提供しておきます。ただ、説明は次回のレッスンまでお待ちください。

今週のチュートリアルの完全なREALbasicプロジェクト(リソースを含む)を手に入れたい場合は、ここからダウンロードしてください。

次週

今度こそSuperDrawを終了することをお約束します!

SuperDrawオブジェクト・コンテスト

SuperDrawのオブジェクト指向によるデザインのため、新しいオブジェクトの追加や現機能の拡張が容易に行えます。その可能性に限りはありません。もうすでにSuperDrawに独自のオブジェクトを追加しましたか?新しいオブジェクトや、現存のオブジェクトの拡張についてのアイデアを何かお持ちですか?それをこちらまでお送りください!

REALbasic Universityではコンテストを開催します!あなたのSuperDrawオブジェクト、機能拡張したものを送ってください。それらを審査し、すばらしい作品には賞品や特典を差し上げます。REALbasic Developerマガジンの無料購読、RBDのTシャツ、その他、すてきな賞品が待っています!

コンテストのルール

受賞者は2003年5月に発表予定ですので、いますぐ作成に取りかかりましょう!

Letters

今回は、モデムでの送受信についての質問があるDarius Monaghanさんからです。

こんにちは。

私はまったくのRBの初心者で、はやくも問題に突き当たってしまいました。私はモデムを使って他のコンピュータにダイヤルし、ユーザ名とパスワードを使ってログインして、ファイルを受け取ることをすべて自動で行うアプリケーションを作成しています。

これまでにダイヤル、接続、ユーザ名とパスワードを送るところまでうまくいきました。ファイルの受信をする段階まできたのですが、どこから手をつけたらよいのかわかりません……。

プロトコルでしょうか?それともダウンロードのディレクトリでしょうか?

どこから始めたらよいのかわからないのです……。

どうすればよいか、何かアドバイスいただけないでしょうか?

よろしくお願いいたします。

Darzz

おもしろい問題です!あなたの書いている方法ですと、シリアルコントロールを使って、実際にモデム経由で接続しています(TCPを使ったインターネット経由ではありません)。しかし、いったん接続してしまえば、どちらもまったく同じように動作するでしょう。

あなたが両方の端末(接続する側とされる側、またはサーバとクライアント)のどちらも記述するのであれば、あなたの使いたいどんなプロトコルでも採用することができます。整合を保っている限りは、それで問題なく機能するはずです。

ところが、接続しようとする端末のプログラムは、おそらく独自の設定とプロトコルをサポートしているので、それと同様のプロトコルをあなた自身でサポートしなければなりません。Macであれば、ファイルはおそらくリソース・フォークを保存するbinhex(binhexフォーマット) になっているでしょう。

あなた自身でプロトコルを書き上げるのもひとつの手ですが、サード・パーティのプログラムを使用することもできます。Einhugurのe-CryptIt Engineは、Base64、BinHex、MacBinary III、AppleSingle / Double、そしてUUCodingで、相互のやりとりが可能です。

端末プログラムにはファイルの送受信で独自のコマンドがあるので、その端末プログラムがサポートしているものを調べる必要があります。

上記の手段は実行可能ではありますが、そう簡単ではありません。私は、あなたが何の目的で、どうしてそれをする必要があるのかはっきりわかりません――現在の状況ならば、TCP経由での通信の方が、モデムでの通信よりもはるかに便利です。たとえば、これがあなたのやろうとしていることであればですが、標準のFTPコマンドを使用すれば、FTPサーバが確実にファイルを送受信してくれます。Pyramid Design社のFTP Suiteのようなよくできているサード・パーティのプログラムを使用すれば、FTPについてあまり勉強する必要もありません。


RBU-Jの通知サービス!コラムが発表されるたびに日本語版REALbasic Universityのお知らせの emailがあなたに届きます。登録・削除は ここ から。

INDEXに戻る