RealBasic University

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

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

OOP University:パート 17

前回のレッスンでは、SuperDrawがどのようにファイルの保存・読み込みを行うかの解説を最後まですることができませんでしたので、今回はそれを行いSuperDrawの仕上げに入りましょう。

ファイルを開く

ファイルの保存ルーチンは完成しましたので、今回は既存のファイルを読み込むための新規メソッドを追加していきましょう。このメソッドは、単にパラメータとしてfolderItemをとり、そこに保存されたSuperDrawの画像を読み込もうとするものです。

  
sub loadFile(theFile as folderItem)
dim binFile as binaryStream
dim i, j, n, kind as integer

if theFile <> nil and theFile.exists then
binFile = theFile.openAsBinaryFile(false)
if binFile <> nil then

// 既存のオブジェクトの消去(もしあれば)
eraseAllObjects

n = binFile.readLong
for i = 1 to n
// オブジェクトkindの読み込み
kind = binFile.readLong

addObject(kind, binFile.readLong, binFile.readLong, binFile.readLong, binFile.readLong)
objectList(i).setLineSize(binFile.readLong)

if binFile.readByte = 1 then
objectList(i).selected = true
end if

select case kind
case 0 // 円
// 追加プロパティはありません
case 1 // 四角形
// 追加プロパティはありません
case 2 // 三角形
// 追加プロパティはありません
case 3 // ポリゴン
// 追加プロパティはありません
case 4 // 画像
j = binFile.readLong
pictClass(objectList(i)).image = setPictureData(binFile.read(j))
case 5 // テキスト
j = binFile.readLong
textClass(objectList(i)).text = binFile.read(j)
j = binFile.readLong
textClass(objectList(i)).textFont = binFile.read(j)
end select
next // i

binFile.close
theDocument = theFile

// 再描画の実行
me.refresh

else
beep
msgBox "Sorry, couldn't open the file."
end if // binFile = nil
end if // theFile = nil
end sub

では、我々が行う最初のことは、渡されたfolderItemが有効(nilでない)であり、それが存在するものであるかを確認することです。その後に、そこからbinaryStreamオブジェクトを作成し、データを読み込みます。このデータ読み込みコマンドは、saveFileメソッドのものとまったく正反対となります。

我々が読み出す最初の項目は、読み込むオブジェクトがいくつであるかを示しているlongであることに気がつくでしょう。その後、それぞれのオブジェクトに対して、我々が読み出しているオブジェクトがどのような種類のものであるかを知らせてくれる番号を最初に読み出します。これは、異なるオブジェクトはデータの総量が異なってきますので重要なことです(たとえば、pictClassのようなオブジェクトでは、画像を表すデータを余分に含んでいます)。

我々が読み出したそれぞれのオブジェクトに対して、保存された情報にしたがってそのプロパティを設定しました。そして最終的に、我々は画像中に存在していた描画オブジェクトのすべてを再ロードしたのです!

最後のステップは、スクリーンを再描画して、新しい画像を表示します。

画像の処理

ファイルの保存・読み込みルーチンの最大の問題は、pictClassオブジェクトです。これはグラフィックをインポートするので、そのグラフィックをファイルに保存することが必要です。結果的に、SuperDrawのファイル・フォーマットは、複数の画像を含むことになります!

しかしこれを行うためには、我々は画像のバイナリーの生データにアクセスできなければなりません。通常では、REALbasicはそれにアクセスする手段を我々に提供していません。あなたがグラフィックを読み込むときは、それはpictureデータとして保持され、実際にはその生データについて知ることはありません―― REALbasicがすべてを水面下で処理しているのです。

それでは、どのように生の画像データを入手すればよいのでしょうか?ええ、それは少し難しい問題ではありますが、基本原理はこうです。我々はグラフィックがディスクに保存されているときは、それは生のバイナリデータとして存在していることを知っています。そして、それはまさに我々の欲しいデータです。REALbasicが画像としていったんそれを読み込んでしまうと、それはデータとして処理するにはもちろん手遅れです。それでは、画像データとしてではなく、バイナリデータとしてディスクから画像を読み込んではどうでしょう?そうしない手はないでしょう!

追加すべきメソッド(drawCanvasClassへ)は、次のようになります:

  
function getPictureData(thePicture as picture) as string
dim f as folderItem
dim bf as binaryStream
dim s as string

if thePicture <> nil then
// 一時ファイルの作成
f = temporaryFolder.child("picture" + generateRandomString)
while f <> nil and f.exists
f = temporaryFolder.child("picture" + generateRandomString)
wend

f.saveAsPicture thePicture
bf = f.openAsBinaryFile(false)
if bf <> nil then
// バイナリデータとして画像の読み込み
s = bf.read(bf.length)
bf.close
f.delete // 一時ファイルの消去
return s
end if // bf = nil
end if // thePicture = nil

return ""
end function

おわかりのように、この関数はパラメータとして画像をとり、文字列(バイナリーの生データ)を返します。これを行うためには、一時ファイルに、グラフィックを画像ファイルとして保存します。それから、ファイルをバイナリデータとして読み返します。

よってこのルーチンが乗り越えなければならない第一の障壁は、グラフィックのための一時ファイルを生成することです。我々はこのファイルをtemporaryFolder内に保存して、それにランダムな名前(このあとすぐに作成するgenerateRandomStringと呼ばれるルーチンを用います)を与えます。既存のファイルを消去しないことを確認するために、我々はそれを作成する前に、そのファイルが存在していないかを確認します。

folderItemが有効であれば、ディスクにファイルを作成するためにREALbasicのsaveAsPictureメソッドを使用します。それから、そのファイルをbinaryStreamファイルとして開き、そのデータをすべて読み出します。一時ファイルを閉じて、消去した後には、取り出したデータを返します。もし何かエラーが生じた場合は、この関数は空の文字列を返します。

これがうまく機能するためには、一時ファイルに名前が必要ですので、次のgenerateRandomStringメソッドを追加しましょう:

  
function generateRandomString() as string
return str(rnd * 9) + str(rnd * 9) + str(rnd * 9) + str(rnd * 9)
end function

これはとてもシンプルです。0から9までの間で4つの数からなる4文字列を返すだけです。直前のコードでは、これは一時ファイルが「picture 0123」(または他の数値)と名付けられることを意味します。

だいぶ進みました。しかし、我々の読み込み・保存メソッドが機能するためには、getPictureDataと反対のものが必要となるでしょう。したがって、パラメータとしてはデータ列をとり、そして画像オブジェクトを返すsetPictureDataを追加しましょう。

  
function setPictureData(theData as string) as picture
dim f as folderItem
dim bf as binaryStream
dim p as picture

// 一時ファイルの作成
f = temporaryFolder.child("picture" + generateRandomString)
while f <> nil and f.exists
f = temporaryFolder.child("picture" + generateRandomString)
wend

bf = f.createBinaryFile("bin")
if bf <> nil then
// 画像データの保存
bf.write theData
bf.close

// 画像として再オープン
p = f.openAsPicture
f.delete // 一時ファイルの消去
return p
end if // bf = nil

// エラー
return nil
end function

おわかりのように、これはその逆のルーチンと非常によく似ています。最初にランダムに生成された名前の一時ファイルを作成し、それからbinaryStreamとしてそこに画像データを保存します。ファイルが書き込まれたら、pictureオブジェクトとしてそれを読み返します。それから、一時ファイルを消去して、pictureオブジェクトを返します。このルーチンに何か問題が発生すれば、nilを返します。

これは読み込み・保存ファイルのそれとよく似ています。しかしながら、私が前回に忠告した、修正・追加されたオブジェクトをサポートするためにファイル・フォーマットを変更することに関しては、記憶にとどめておくことが重要です。

私はいま追加したばかりのこの3つのルーチン――getPictureDatasetPictureData、そしてgenerateRandomString――をPrivate(REALbasic 5以降では「Protected」と呼ばれます)にすることをお薦めします。これは、これらのメソッドをdrawCanvasClassの外部から利用することを防ぎます。これらは他のオブジェクトがアクセスする必要のないルーチンですので、これはよい考え方です。

最後の仕上げと、今後の修正

RBUの目的であるSuperDrawは、これで終了しました。(しかし5月にはSuperDrawコンテストの結果を発表する予定です――コンテストの詳細については今回のニュースをご覧ください)。

しかし終了する前に、あなたがSuperDrawをより完成された描画プログラムに拡張しようと考えている場合に備えて、私はSuperDrawの2、3の制約と、潜在的な問題について触れようと思います。このリストはもちろん、完全なものではありません――あなたがSuperDrawのクラスを使用しようとしたときに直面する、他の重大な問題がある可能性もあります――しかし、このような問題を考えることは、よくある問題について、そして真に洗練されたプログラムを作成するのに何が必要かを考えるのに役立つと思います。

ドラッグに関するバグ

最初に、ドラッグのシステムに関する潜在的な問題がありました。現在では、我々は描画領域の外部のオブジェクトをドラッグすることは許可していません――それは、mouseDragイベントはcanvas1の内部でのみ機能するからです。しかしながら、我々は一度に複数のオブジェクトを選択でき、それをグループとして移動できることから、余分なオブジェクトのうちのひとつを描画領域の外に追いやってしまう原因となります。一度そうなってしまうと、それを元に戻す方法はありません!

ユーザーは隠れたオブジェクトを表に出すために、描画領域を大きくしたり、小さくしたりできます。しかし、ある時点で、描画領域のサイズがユーザーのスクリーンのサイズに限定された時点で、もちろん、オブジェクトはその最大サイズからはみ出す可能性があります。

これを改善するためには、オブジェクトが描画領域の外(canvas1の外)に出ないように防止するコードを追加しなければなりません。

画像の比率

もうひとつの問題は、pictClassは画像の比率に関して注意を払わないことです。画像ボックスが50×50であり、我々はそこに50×100の画像をインポートするとすれば、その画像は50×50に縮小されます。ユーザーはもちろん、自由にこれを調整でき、好きなように画像を伸ばしたり縮めたりできます。しかし現在のところ、原画像のサイズと正しい比率を保つ確実な方法はありません。

Macintoshの標準では、シフトキーが押されている間にサイズ変更された場合、画像はその比率が固定されます。この機能を追加することを、強くお薦めいたします。

オブジェクト・レイヤー

最後に、あなたはSuperDrawの描画オブジェクトが、お互い重り合って見えると思います。現在のところ、これはオブジェクトの追加された順番にもとづいて起こります。最初のオブジェクトは下の方になり、新しいオブジェクトはその上に現れます。

しかし、ユーザーが間違った順番でこれらを作成した場合はどうなるのでしょう?現時点では、ユーザーは消去してやり直すしかありません。専門的な描画プログラムは、ユーザーにレイヤーでオブジェクトを前や後(あるいはいちばん上や下)に移動させています。オブジェクトのレイヤー機能を追加することは、素晴らしいことで、それもさほど難しいことではないでしょう。

レイヤーの上下でオブジェクトを移動することは、原理的には難しいことではありません。それは単に、objectList配列のオブジェクトを上下させるだけです。例えばobjectList(10)objectList(9)になります。しかし、あなたがこのOOP Universityシリーズのはじめの方を覚えていればおわかりのように、REALbasicのオブジェクトは、インスタンスではなく参照によってリンクされています。したがって、objectList(10)はオブジェクトそのものではなく、オブジェクトへの参照となります。あなたはただのobjectList(10) = tempObjectコマンドによって、オブジェクトを再定義することはできません。そうではなく、あなたはオブジェクトをコピーする必要があり、そのコピーとは、そのプロパティすべてをコピーすることです。これを行うもっともよい方法は、shapeClasscopyToメソッドを追加し、それから、独自のプロパティをもつサブクラス(textClassなど)内でプロパティをコピーすることで、そのメソッドを上書きします。

またこれらレイヤーの移動を処理するインターフェースが必要です。もっとも一般的なものは、メニューコマンド(あるいはおそらくコンテキスト・メニュー)です。そこでは、drawCanvasClassに選択されたオブジェクトを上下あるいは最上部・最下部に移動するよう指示するので、あなたはこれらのアクションのすべてに対するメソッドを用意する必要があることがわかるでしょう。それは難しくはありませんが、いくつかのステップを必要とします。

このレイヤーの問題を解決する、もうひとつの別のアプローチは、SuperDrawをオブジェクトのオーバーラップ(お互いの交差)が起こらないように、単に防ぐことです。これは次の2つの場所で行う必要があるでしょう。addObjectメソッドでは、新しいオブジェクトが他のものとオーバーラップしないよう確認します。そしてmouseDragでは、移動したオブジェクトが、他のものの上に移動していないか確認します。どちらの方法をとっても、乗り越えるためには大きな問題を解決する必要があります。

最後に

SuperDrawであなたが何を行いたいかによって、これらはあなたが修正したいものでなかったり、もっと他に重要なものがあったかもしれません。それはあなた次第です。しかし願わくは、我々は問題点を十分に取り上げ、あなたがどのようにあなたの望むSuperDrawに仕上げていけばよいか、アイデアを得たことと思います。

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

次週

今まで通り、懐かしのOOPレクチャーに戻ります!

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

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

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

コンテストのルール

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

Letters

今回は、ファイルの保存に関連した質問をお持ちのMel Defrancescoさんからメールを頂きました。

こんにちは、Marcさん。

私はあなたのコラムを楽しんでいて、この問題をあなたが助けてくれないかと思ったのでお手紙します。私はそれは手間のかかる問題ではないと思うのですが、解決策がわかりません(あまりにも明らかなことで私がネアンデルタール人のように見られないことを祈ります)。問題はこうです――。

次のものは、変数「percentcomplete」の情報が、読み込み・保存される2つのメソッドを示しています。問題は、「Save As」がすでに実行されたために「filename」が確定されている場合、「save」ルーチンがすでに作成されたファイルを見つけ、それを新しいアップデートされたファイルと置き換えて、その後の変更を保存するためにはどのようにすればよいのでしょうか?

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

LOAD METHOD:

  
dim file as folderItem
dim binary as binaryStream
dim i as integer
redim percentcomplete(-1)

file=getopenFolderItem("filetype")
if file<>nil then
file.extensionVisible=true
filename=file.displayName
binary=file.openAsBinaryFile(false)
do until binary.eof
i=binary.readByte
percentcomplete.append val(binary.read(i))
loop
filehasbeensavedbefore=true
end if

SAVE METHOD:

  
dim binary as binaryStream
dim file as folderItem
dim i as integer

if filehasbeensavedbefore=false then
file = getsaveFolderItem("filetype",filename)
if file <> nil then
filename=file.displayName
binary=file.createBinaryFile("filetype")
for i = 0 to 22
binary.writeByte len(str(percentcomplete(i)))
binary.write str(percentcomplete(i))
next
binary.close
end if

else if filehasbeensavedbefore=true then
//ここはうまくいかない部分です
file = getFolderItem(filename)
if file.exists then
binary=file.createBinaryFile("file")
for i = 0 to 22
binary.writeByte len(str(percentcomplete(i)))
binary.write str(percentcomplete(i))
next
binary.close
end if
end if

filehasbeensavedbefore=true

Melさん、いい質問です。私が今日まで、今回のSuperDrawの完了まで質問を保留していたことを気になさらないようお願いします。

最初に言わせて頂きますと、私はあなたがどのような問題に直面しているのかはっきりわかりません。あなたは、コードの「save as」の箇所がうまくいかず、それがなぜだかわからないと仰っています。プロジェクトの複製を作成するか、あなたの完全なコードなしでは、私にとってこれをテストすることは困難です。しかし、あなたのコードにいくつかの問題点が目につきましたので、いくつか私の見解を述べようと思います。

私はあなたのコードに、2つの潜在的なバグを発見しました。「save as」コードで、あなたは次のように書いてします:

  
binary=file.createBinaryFile("file")

ここでは、「file」というファイル・タイプを使用しています――ところが、読み込み・保存ルーチンでは、あなたは「filetype」というファイル・タイプを使用しています――それが問題であるかもしれません。

問題の別の可能性は、「save as」ルーチンで、あなたはファイルのfolderItemを次のコードで再び生成しようとしています:

  
file = getFolderItem(filename)

ここでの問題は、「保存」と「読み込み」コードを見れば、あなたは次の行を見つけるでしょう:

  
filename=file.displayName

ここでの問題は、これはfilenameにパスではなく、ファイル名を設定していることです。さらに悪いことに、これはdisplayNameだけなので、ユーザーが拡張子を不可視にした場合、Mac OS Xでは不完全になりうることです。

これらの2つのコードをまとめると、「save as」ルーチンのコードは、実際には次のように宣言していることになります:

  
file = getFolderItem("a sample file")

問題は、getFolderItemはファイル名ではなく、ファイルへの完全なパスが必要だということです。ここではただファイル名をとり、そしてそのファイルはアプリケーションと同じフォルダにあると仮定しています。したがって、あなたのルーチンはアプリケーションと同じフォルダにファイルがあればうまく機能しますが、ハードディスクのどこかにファイルがある場合は、確実に停止するでしょう。

これに対するひとつの解決策は、filename=file.displayNameを次で置き換えることです:

  
filename=file.absolutePath

これは名前だけでなく、ファイルへの完全なパスを記憶するので、問題はこれで解決するでしょう。

ところが、あなたが検討したいと思われる、もっと簡潔な解決策があります。私はこれを今回のSuperDrawのレッスンの一部として解説しようかと考えましたが、実際はSuperDrawプログラムの一部ではなく、それはほとんどすべてのプログラムに適応できることなので除きました。

SuperDrawでは、我々はひとつの「File Save」メニューを作成することだけでも悩んだことは知っているでしょう。実際のMacintoshのアプリケーションでは、「Save」と「Save As...」の2つのメニューを持つべきです。もっともそれがあなたのアプリが持つもので、そしてあなたが困っているところです。

さてオブジェクト指向本来の様式では、ドキュメントは自分自身でどのように保存するのか知らなければなりません。それが、SuperDrawdrawCanvasClassが独自の保存・読み込みルーチンを持つ理由です。しかし、より完成されたものにするためには、「save as」ルーチンを同様に設けるべきです。

あなたのコードでは、あなたは「save」と「save as」のコードを同じルーチンに入力しています。これはまったく駄目ということではありませんが、あなたはコードをそっくり真似ています(2つのセクションのコードがどれくらい同じであるか見てください)。次に、これを行うもっとよい方法があります。

我々がSuperDrawで行ったように、保存されたファイル(それをtheDocumentと呼びましょう)へのリンクであるfolderItemプロパティを追加してください。これをあなたのコードすべてのローカルなfile変数と置き換えます。あなたがこれを行えば、filehasbeensavedbefore変数も同様に取り除くことができます。それはtheDocumentが保存されたファイルを指し示すか、またはそれがnilとなるからです。それがnilであれば、ファイルはまだ保存されていないことを意味します(ちょうどあなたのfilehasbeensavedbefore変数と同じように)。

そこで、あなたに必要なすべてのコードは、次のコードに近いものです(もちろん、あなたのプログラムでは少し違ったコードが必要になるでしょう):

FileSaveAsメニュー・ハンドラー:

  
if theDocument = nil then
theDocument = getsaveFolderItem("filetype", "untitled")
if theDocument <> nil then
doFileSave
end if
end if

FileSaveメニュー・ハンドラー:

  
doFileSave

FileOpenメニュー・ハンドラー:

  
doFileLoad

FileSaveメニュー・ハンドラー内では、いかなるチェックもしていないのは、次のEnableMenuItems内でチェックを行うべきだからです。

  
FileOpen.enabled = true // 常に利用可能
FileSaveAs.enabled = true // 常に利用可能
if theDocument <> nil then
// ドキュメントがすでに保存された場合のみ利用可能
FileSave.enabled = true
end if

これはまだファイルが保存されていない場合、FileSaveメニュー・コマンドが利用可能となることを防止します(「Save As...」が利用可能な唯一の選択肢です)。

我々は保存ルーチンの外部で、エラーチェックの大部分を行っていますので、実際のdoFileSaveメソッドはもっと簡潔で、一般的になります:

doFileSaveメソッド:

  
dim binary as binaryStream
dim i as integer

binary = theDocument.createBinaryFile("filetype")
for i = 0 to 22
binary.writeByte len(str(percentcomplete(i)))
binary.write str(percentcomplete(i))
next
binary.close

このルーチンはtheDocumentが有効でない限り呼ばれないので、このルーチンはtheDocumentnilであるかないかを確認する必要もないことに気づくでしょう。我々はまた、これが「save as」であるかないかを確認する必要もありません。それは、このルーチンが、最初の保存であろうと100回目の保存であろうと同じ事を行い、その違いを問題としないからです。

これで、あなたのコードがより簡単になっただけでなく、これでたったひとつのルーチンにまとまりました。したがって、あなたがファイル・フォーマットを変更した場合は、たったひとつの場所のコードをアップデートするだけで済みます。

あなたのファイル読み込みルーチンは少し修正が必要ですが、ほとんどは同じです。主な違いは、ローカル変数のfileの代わりに、開かれたファイルのfolderItemtheDocumentに保存することです:

doFileLoadメソッド:

  
dim binary as binaryStream
dim i as integer

redim percentcomplete(-1)
theDocument = getopenFolderItem("filetype")
if theDocument <> nil then
theDocument.extensionVisible = true
filename = theDocument.displayName
binary = theDocument.openAsBinaryFile(false)
do until binary.eof
i = binary.readByte
percentcomplete.append val(binary.read(i))
loop
end if

これであなたの問題が解決され、コードが改善されること望みます。この方法は、十分に一般的なもので、あなたはすべてのプログラムに対して、同様のテクニックを使うことができます。それは非常に簡潔で、「save as」問題を取り扱うにはお薦めのアプローチです。これを機能させるにはどうすればよいかを知るには、それは紛れもなく役立つスキルです。

興味のある人たちは、上記のテクニックを用いて、SuperDrawに「save as」機能を追加してみるといいでしょう。SuperDrawはコードの大部分がカプセル化されているので、手順は上記の例題とあまり変わりありません。

たとえば、drawCanvasClassのプロパティであるtheDocumentprivateにするべきです――それで外部ルーチン(たとえばWindow1)が、theDocumentnilであるかないかをチェックできません。その代わり、あなたはtheDocumentnilであるかないか知らせるdrawCanvasClass内に関数を追加せねばなりません(それをbooleanを返り値とするhasFileBeenSavedと呼びましょう)。

そうすれば、それらのハンドラーはdrawCanvasClassのプロパティに適切に直接アクセスできます。もうひとつのメソッドは、ファイルメニュー・ハンドラーをdrawCanvasClass内へ移動することでしょう。


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

INDEXに戻る