RealBasic University

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

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

OOP University:パート 11

前回、我々は描画オブジェクトをキャンバス・オブジェクトのクラス内に移動させるという重要なステップにより、SimpleDrawSuperDrawへと変化させるプロセスを開始しました。今回は、オブジェクトに新たな機能を追加します。それにより、オブジェクト指向プログラムがどれほど強力であるかを垣間見ることになるでしょう。

描画要素の選択/解除

始めに、もうすでにSimpleDrawではないので、前回作成したファイル名をSuperDraw.rbと変更しましょう。

さて、我々が追加する主な機能は、描画要素を選択/解除する機能です。これを行うためにはdrawCanvasClassオブジェクトを使用して、mouseDownイベントを横取りし、それらを描画オブジェクトに渡します。しかしここで、オブジェクトが本来の方法で使われるもっと賢いやり方があります:drawCanvasClass内で「このクリックはオブジェクト内部でなされたのか?」の計算をする代わりに、クリックされたかどうかをオブジェクトに尋ねるのです!

我々はまた、オブジェクトが選択された時に表示される選択ハンドルを描画するルーチンを、オブジェクトに追加する必要があります。

これは幾分複雑で、困難がつきまとうので、心しておいてください。この種のコードの一つの欠点は、中間点が無いということです:うまく行くか行かないかのいずれかになります。それはつまり、すべてを書き上げるまでコードのテストができないということが、この種のコードを書くのを難くしているのです。しかし、私がついています。それでは、開始しましょう!

描画要素オブジェクト

描画要素に機能を追加することから開始しましょう。我々は、circleClassrectClass、そしてtriangleClassの3種類の描画オブジェクトを持っています(shapeClassは、何も描画しないので数に含めません)。これらのオブジェクトに、機能を追加していきたいと思います。特に、オブジェクトがクリックされたかどうかを伝えるだけでなく、それが選択されたかどうかを表示する機能です。しかし、OOP原理である継承のため、我々は親オブジェクトであるshapeClassのみに注目すればよいのです。そこへ追加するどのような機能も、サブオブジェクトに自動的に継承されます!

始めに、shapeClassの新規プロパティ(それをダブル・クリックして、編集メニューから「プロパティの追加」を選択します)としてselected as booleanを追加します。このプロパティがtrueならば、描画要素が選択されていることになります。

今のうちに、このプロパティをトグルするメソッドを追加しましょう。新規メソッドを追加し(編集メニューの「新規メソッド」)、それをtoggleSelectionとします。それに次のコードを与えてください:

  
selected = not selected

次に、もう一つのメソッド、パラメータをg as graphicsとしたdrawSelectionを追加します:

  
sub drawSelection(g as graphics)
const w = 4

if selected then
// Draw selection handles
g.foreColor = rgb(255, 0, 0) // Red
g.fillRect(left - w, top - w, w, w)
g.fillRect(left + width, top - w, w, w)
g.fillRect(left + width, top + height, w, w)
g.fillRect(left - w, top + height, w, w)
g.foreColor = rgb(0, 0, 0) // Black
end if // selected
end sub

これはグラフィクス・オブジェクトをパラメータとして入力し、オブジェクトの四隅に赤い四角形を描画することに注目しましょう。これらは選択ハンドルです。オブジェクトが選択されたとき、ハンドルが現れ、オブジェクトが選択されたことをユーザに伝えてくれます(最終的にこのハンドルは、ユーザにオブジェクトのサイズを変更をさせることにも使われます)。この選択ハンドルのサイズは、定数wによって設定されます。

もちろん、このメソッドは呼ばれない(実行されない)限りは何の役にも立ちませんので、それを呼ぶためのコードを追加する必要があります。shapeClassdrawメソッドを開いてください。前回の指示に従っていれば、shapeClassは何も描画しないので、ここは全くの空のはずです。

しかし、我々はいまそれでdrawSelectionを呼び出したいのです。すべての描画オブジェクトは、同じ選択ハンドル・システムを使用しているので、ここに次のコードを入力することができます:

  
// ShapeClassは実際は何も描画しないので、
// サブオブジェクトに描画を伝え 
// (必要であれば)その後、選択ハンドルを描画します。

drawSelection(g)

さてここで、OOPの第一関門にぶつかります。shapeClassのそれぞれのサブオブジェクトに、自分自身のdrawメソッドを実行させるためにoverridingを使用したことを覚えているでしょうか。shapeClassのdrawメソッドにコードを入力しても、それの代わりにサブオブジェクトのdrawメソッドがそれぞれ呼ばれるので、それは永久に実行されません!

これに対する分かりやすい回避方法は、それぞれのサブオブジェクトのdrawメソッドで、drawSelectionを呼ぶことです。しかし、それはあまりにもOOP様式ではありませんよね?4つのオブジェクトそれぞれに、同一のコードを追加する必要があるだけでなく、これから作成するであろうオブジェクトに対してもそのコードを追加しなければなりません。そうです、OOPというもっと優れた方法があるのです。

我々がしようとしていることはこうです:shapeClassに新規のイベントを追加します。shapeClassesのすべてのサブオブジェクトは、そのイベントを継承します。我々はそれぞれのサブオブジェクトのイベントに、その描画コードを入力すればよいのです。

注意深く目を通してください。shapeClassを開き、編集メニューへ行き、「新規イベント」を選択します。そのイベントに、パラメータをg as graphicsとしてpaintと命名します。さて、shapeClassのdrawメソッド内で、最終行の上にpaint gの1行を挿入してください。コメントを無視して、drawメソッドは次のようになります:

  
paint g
drawSelection(g)

完璧です。それではcircleClassを開いてください。イベントの隣の三角形をトグルすれば、(驚くことに)paintと呼ばれるイベントが見つかるでしょう。ここがクールなところです:これはイベントを追加したshapeClassのサブクラスなので、このイベントにコードを追加することができます!

circleClassのdrawメソッドに行き、すべてのテキストを選択してください。それをpaintイベントへカット&ペーストしてください。次のようになります:

  
sub paint(g as graphics)
g.drawOval left, top, width, height
end sub

次に、circleClassからdrawメソッドを削除してください。(これは重要なステップです!忘れないでください!)

このステップをrectClasstriangleClassに対しても行います。drawメソッドからpaintイベントへコードを移して、drawメソッドを削除してください。

SuperDrawを実行すれば、以前とまったく同じように動作することが分かるでしょう。何も変わりがありません!それは、我々のコードはオブジェクトのdrawメソッドを呼ぶからです。サブクラスはdrawメソッドをもう持っていないので、shapeClassのデフォルトのdrawメソッドが使用されます。shapeClassの新しいdrawメソッドは、サブクラスに自分自身を描画するよう伝えるだけでなく、(必要であれば)選択ハンドルも描画します。今現在は、オブジェクトが選択されたことを伝える方法がないので、プログラムは以前とまったく同じように動作します。

クリックがなされたか?

言うまでもなく、オブジェクトがクリックされたかどうかを検知する方法が必要です。よって、これをチェックする簡単なメソッドを追加しましょう。

shapeClassに、新規メソッドを追加(編集メニューの「新規メソッド」)してください。その名前をwithinMeとして、パラメータをg as graphics, x as integer, y as integer、そしてそれにbooleanを返させます。ルーチンは次のようになります:

  
function withinMe(g as graphics, x as integer, y as integer) as boolean
dim w, h as integer

w = left + width
h = top + height
if x >= left and x <= w and y >= top and y <= h then
return true
end if
end function

これは何を行うのでしょうか?それは、クリック座標である横軸と縦軸の値が渡されます。そして、クリックがオブジェクトの内側にあるかどうかを簡単に確認します。xyの値がどちらともその内側にあれば、trueを返します。そうでなければ、返される初期値はfalseとなります。

これで終わりです!これで我々は、「このクリックは内側でなされたか?」をオブジェクトに尋ねる方法を手にしたことになり、オブジェクトはイエスかノーで答えてくれるでしょう!

オブジェクトのクリック処理

しかしながら、どのように実際のクリック座標を入手し、それをオブジェクトに渡すことができるのでしょうか?それは簡単です!drawCanvasClassオブジェクトは、REALbasicからmouseDownイベントを受信することを覚えているでしょうか?我々はただ、これらを途中で横取りして、それを処理するだけです!

drawCanvasClassを開いてください。mouseDownイベントで、次のコードを入力してください:

  
dim i, n as integer

currentObject = new shapeClass
n = objectCount

// オブジェクトの内側であるか?
// 逆方向にスキャンするので、最後(最上部)のオブジェクトが始めに調べられます。
for i = n downto 1
if objectList(i).withinMe(me.graphics, x, y) then
// currentObjectはクリックされたオブジェクトを示します。
currentObject = objectList(i)
currentObject.toggleSelection
currentObject.draw(me.graphics)
xStart = x
yStart = y

// オブジェクトを見つけたので、ループを続ける必要はありません。
return true
end if
next // i

// どのオブジェクトもクリックされていないので、すべてを解除します。
for i = n downto 1
objectList(i).selected = false
next // i
me.refresh

簡単にこれが何を行うか説明していきましょう。まず始めに、動作確認をしましょう。もう一つのメソッドを追加してください:

  
function objectCount() as integer
return uBound(objectList)
end function

次のコードをmouseUpイベントに入力します:

  
me.refreshRect(currentObject.left - 4, currentObject.top - 4, currentObject.width + 8, currentObject.height + 8)

最後に、次のプロパティをdrawCanvasClassに追加します:

  
currentObject as shapeClass
xStart as integer
yStart as integer

それでは、プログラムを実行してください。描画要素をいくつか追加して、それをクリックするとどうなるのか確認してください。すべてを正しく行っていれば、それは選択、解除をするはずです。ウィンドウ画面は次のように見えているでしょう:

なかなかかっこよくありませんか?あなたが円、三角形、あるいは四角形のどれをクリックしても、オブジェクトには関係なく、すべては同じように動作します。shapeClassに基づいて新規のオブジェクトを追加しても(後に行う予定です)、すべて同様に動作するでしょう!

今週はこれで終わりです:次回は、選択されたオブジェクトを移動できるように改造します(さらに他の多くの機能も追加します)。

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

次週

SuperDrawにいくつかの素晴らしい機能を追加します。

RBU難問

ここにデバッグについての問題があります。今まで何か不明な理由でルーチンが機能しなかった経験がありますか?コードはとても単純です――文字列を抜き出し、それを処理して結果を返します――しかし、おかしいことに操作がうまく機能しないようなのです。

実際の例題を見てみることにしましょう。テキストを取り込んで、すべてのHTMLタグ(< >記号の間のテキスト)を抜き取るルーチンを書きました。次はそのコードです:

  
function stripHTML(s as string) as string
dim s, t as string
dim i, j as integer

// この機能は渡された文字列からHTMLを
// 抜き取ります。

// 渡されたパラメータsは加工できないので、
// それをtにコピーし、それを加工します。
t = s
// 始めの<を探す
i = inStr(t, "<")
while i > 0
// 位置iから始めて、>を探す
j = inStr(i, t, ">")
if j = 0 then
exit // 完了。
end if

// HTMLタグを抜き取ります。
// mid()に長さを与えないと、
// 残りの文字列を返します。
t = left(t, i - 1) + mid(t, j + 1)

// iから始めて検索を継続
i = inStr(i, t, "<")
wend

return t
end function

コードは正しいようです。しかしこれを実行すると、空の文字列が返されます。処理がまったく行われていないようです。何が起こったのでしょう?バグはどこにあるのでしょうか?

しばらくの間、あなたに考える時間を与えたいと思います。お望みならば、こちらに今回のプロジェクトがあります。

降参する場合は、解答はレターセクションの後のこのページの最後にあります。

Letters

今週はRBUの未来に関する2つの提案をいただきました。始めに、Jefさんからです:

Marcさん、

私はREALbasicのまったくの初心者で、あなたのレッスンを通して徐々にやりたいことを学んでいます。現時点で多くのレッスンがあるので、今回の質問についてすでに述べられているのか見当がつきません。

私は基礎的なデータベースのフロントエンドを作成したいと思っています。基本的には、辞書のようにいくつかの異なるリストがあります。一つのEditField(あるいは プル・ダウンのメニュー)に用語を表示し、それに対応する情報(例えば、用語の定義)をもう一つのEditFieldに表示させたいと思います。いままでに、このようなことを扱ったレッスンはありますか?

- Jef

こんにちはJefさん!私はまだ特にRBUではデータベースを取り扱っていません。しかしデータベースの形式であるデータ構造は、数多く扱ってきました。データベースは、私がほとんど経験していないものですが、それについては現在学んでいるので、2003年にはレッスンができると思います。

それまでは、データベース記事を毎号掲載しているREALbasic Developerマガジンの定期購読を(いつものように)提案したいと思います。

あなたの場合には、私はデータベースが必要であるのか分かりません。事実、私がRBU用語集のページを制作するために書いたRBU用語集プログラムに非常に似ているようです。プログラムは実行すると、次のように見えます:

これは、用語と定義の追加・編集ができ、Publishボタンを押すとすべてのインデックスにリンクが付加されたHTMLファイルを生成します。実際の用語と定義はテキストファイルに、用語一つにつき1行の割合で保存されます。データベースは必要としません。

興味があれば、いつかこのプログラムのソースを公開したいと思います。興味があるかお知らせください。

次のお手紙は、3DについてのDouglas Beagleyさんからの質問です:

こんにちは。私は、REALBasicの3D機能についてのあなたの意見に興味があります。私はJoe Stroutさんの興味深いFAQを見つけましたが、REALBasicの3Dツールで何ができて何ができないのか、説明や批評が書いてあるものはほとんど見つけられませんでした。REALBasicのウェブサイトで検索しても、ほんのわずかしか検索できませんでした。

これは今後のRBUのテーマなのでしょうか?そうこうしているうちに、私はあなたのサイトにたどり着き、素晴らしい解説を自分のものにするために、充実した時間を過ごしています。

-douglas

Douglasさん、ご提案ありがとうございます。残念なことに、私の3Dに関する知識は、データベースの知識よりもありません!私は知っていることについてだけ書きます。私が3Dについて勉強すれば、それについてのコラムを書くことをお約束しますが、すぐには期待しないでください。

ところで、ここで延々と宣伝を繰り返すのは気が引けるのですが、REALbasic Developerマガジンの購読もご検討してみてください。Joe Nastasi氏(idevgamesでもいくつか3Dの記事を書いています)による初心者向けの3Dのコラムが毎号掲載されます。また、Joe Strout氏による第1.2巻に掲載の3D Math Primerや、第1.3巻に掲載の射撃ゲームのような3Dの記事を時々掲載もしています。

私がしたいと思っていることを全てできないというのが、雑誌を出した理由の一部です。それは私に時間や資源がないだけではなく、私は全知全能ではないからです。だから、皆さんにはすでに存在している資源を有効に活用していただきたいと思います。

それについて言うならば、このテーマについてのRBUの今後の情報に目を光らせておきましょう。

難問解答

私が出題したデバックの問題にとても手こずるのは、それがあまりにも自明だからです。それは、一つのセンテンスにtheが2つあり、それにほとんどの人が気づかない校正の難しさようなものです。

この場合では、我々は変数sを二度定義しました:一度はパラメータとして、もう一度はメソッドの変数としてです:

  
function stripHTML(s as string) as string
dim s, t as string

ここでは、sがルーチンに取り込まれると、それは変換したいテキストで満たされることになります。しかし、その後のdim文で、初期値が空である変数にsが置き換えられます。ルーチンでのコードはまったく問題はなかったのですが、変換するためのテキストがただ入手できていなかったのです!

難しかったですね。この種のミスには気を付けましょう。これを避ける一つの方法は、うっかり再利用してしまうsiのような簡単な変数にする代わりに、説明のついた名前を変数に使用することです。


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

INDEXに戻る