プログラム講座 中級編12

- お絵かきソフトを作ろう(その1) -

 中級編12です。今回から数回に渡って「お絵かきソフト」を作成していきます。お絵かきソフトといっても、複雑なことはせずに主に「QuickDraw」の使い方、アンドゥの方法、JPEG画像の読み込みなどを取り扱います。今回は、その1という事で最も基本的な「点を打つ」という処理を行います。なお、今回から中級編7で作成した「画像加工アプリケーション」のプログラムを流用(ほとんど、そのまま)しています。よって、中級編7までは一応理解しているものとして解説していきます。今までと重なる部分については、あまり解説しませんのでご了承ください。




◆点を打つには?
 中級編7の画像加工アプリケーションは、画像に対して特殊効果等を行うために作成したものですが、少しの手直しでペイントソフトに改良する事ができます。
 お絵かきソフトには、最低限どんな機能が必要でしょうか?
「ブラシ機能?」「ペイント(塗りつぶし)缶?」「ライン(線)ツール?」「楕円描画?」「矩形描画?」「パターン描画?」「画像のスクロール?」・・・いえいえ、最低限のお絵かきソフトは「点を打つ」機能だけです。最低限点を打つ事ができれば、絵は描けます。極論だ、という人もいるでしょうがパソコンが8ビットの時代は、実際に点を表示するだけのものも結構ありました。
 という事で、今回作成するのは「マウスでクリックまたはドラッグしたら点を打つ(表示)」部分です。
 点を打つためには、なんと言ってもマウスの位置を求める必要があります。マウスの位置を求めるには、都合のよい事にMOUSE(_horz), MOUSE(_vert)でウィンドウ上の座標が求める事ができると書いてあります。が、笑えない事に「HANDLEEVENTSを使わなければ」という但し書きがついています(泣)。一体何のための命令?と疑いたくなりますが、Macはマルチタスクではないので「マウスボタンが押されている間、追従する」という処理が可能になっています(許されています)。これはマニュアルのサンプルでも、そうなっていますので、まあ仕方がない所です・・・。
 今回は将来の部分も考慮してMOUSE命令を使わずに直接Toolboxを呼び出してマウス座標を求めることにしましょう。



◆レコードと構造体
 今回は今までほとんど使っていなかった「レコード」を使います。レコードといっても円盤の方のレコードではありません(^^;
 C言語をご存じの方ならば「構造体」と書けばわかってもらえると思います。ただし、レコード内では配列が使えない等の制限があります。
 レコードについてはマニュアルでも、結構ページを裂いて説明していますのでマニュアルを一読することをお奨めします。レコードを使用する、宣言するにはDIM文を使います。配列の確保と全く同じ命令です。
 マウス座標を確保する例は幸いにしてマニュアルに記載されています。まずDIM myPoint.4でメモリを4バイト確保します。8バイト確保しても1024バイト確保しても構いませんが、その場合先頭の4バイトしか使用されません。
 レコードを宣言したらToolboxのGETMOUSEを呼び出します。これで確保したレコードにマウスの座標が入ります。ここからX,Y座標を取り出す必要があります。都合の良いことにmyPoint.h%とすると水平座標を求めることが出来ます。.hはあらかじめ定義されているので、プログラム中で宣言しなくても使用する事ができます。その他rect(矩形座標)のtop,bottomなども同様に特に定義しなくても使用する事が出来ます。実際のプログラムは以下のようになります。

DIM myPoint.4: ' "マウス位置を取得するためのポイントレコードを用意"
CALL GETMOUSE(myPoint): ' "マウス位置を取得(ローカル座標になります)"
x% = myPoint.h%: ' "ウィンドウ上でのマウスのX座標"
y% = myPoint.v%: ' "ウィンドウ上でのマウスのY座標"

 マウスの座標はカレントウィンドウ上の「ローカル座標」になります。今回はウィンドウは1枚しかありませんが、マルチウィンドウの場合は注意する必要があります。これでマウスの座標を求める事ができましたので、後は点を打つだけです。点を打つ関数はすでに中級編7で作成してあるので、多少改良して使用します。中級編7の関数はオフスクリーンの範囲外でも描画してしまいますので、オフスクリーン外には描画しないようにチェックしています。



◆ウィンドウに入ったらカーソル形状を変える
 よくウィンドウ上にあるオブジェクトに応じてマウスカーソルの形状が変化するアプリケーションがあります。マウスカーソルを変化させるにはCURSOR命令を使用します。マウス形状を変化させる例はマニュアルにもあり、主にエディットフィールド等でフィールド内に入ったら変化させるという具合になっています。ところが、今回作成する(していく)プログラムには「ピクチャーフィールド」もなければ「エディットフィールド」もありません。
 こういう場合は「マウス座標から求める」方法を採ります。現在のウィンドウサイズ内かつX,Y座標が0以上であればカーソルを変化させ、それ以外の場合は通常の矢印カーソルにします。この処理は以下のようになっています。

LONG IF ( (x% >=0 ) AND (x% < gImageX) ) AND ( (y% >= 0 ) AND (y% < gImageY) )
 CURSOR _penCursor: ' "ペンカーソルに変更"
XELSE
 CURSOR _arrowCursor: ' "通常の矢印カーソルに変更"
END IF

 gImageXには画像の横幅、gImageYには画像の縦幅が入っていますので、これらと比較すればマウス座標がウィンドウ内かどうか調べる事ができます(マウスカーソル座標はウィンドウ上のローカル座標を返しますので)。

 マウスカーソルの形状を任意のものにする場合、あらかじめカーソルリソースを作成しておく必要があります。カーソルリソースは「CURS」です。好きなカーソルを作りましょう。あまりへんてこりんなものは困りものですがf(^^;



◆ON MOUSE FN
  最後にマウスボタンがクリックされた場合の処理(関数)を呼び出さないと何も起こりません。問題はどこでいつ呼び出せばよいのか?という事です。マニュアルを見るとマウスクリック、ドラッグ等の処理は「ON MOUSE FN」を使えばよさそうです。ところが実際に定義してみればわかりますが、うまく機能しません。というのも、「マウスイベントが発生した」時しか呼び出されないためです。イベントが発生していない場合でも(マウスの追従等)機能しなければなりません。
 このような場合は、HANDLEEVENTSの前後で呼び出すようにすればOKです。つまり

DO
 FN drawPoint: ' "マウスによる描画"
 HANDLEEVENTS: ' "イベント処理は自動"
UNTIL gQuit_flag

 とすればよいのです。このようにしておく事でアイドリング処理等を行うことが可能になります。特に半透明のブラシやエアブラシなど処理に時間がかかるものは、座標等を記録しておいて何もイベントおよびマウスが移動していない場合に処理してしまう事ができます。アイドリング処理については、また後の講座で解説します。



◆終わりに
 ちょっと使ってみればわかると思いますが、本当に点を打つだけでは、普通のお絵かきソフトのように線を描けない事がわかるでしょう。普通のお絵描きソフトでは、点と点は「ライン」で結ぶことにより描画しているためです。次回はライン等を描画するために必要な「QuickDraw」について説明します。ただし、深入りはせずに実際の使い方を説明します。最近ではQuickDraw程度では役不足となってしまいました。RhapsodyではDisplay PostScriptを使用(採用)しているのでQuickDrawと比べて多彩な描画を行う事ができます。しかし、画像加工や特殊効果などは装備していないため、今回のようなお絵かきソフトや画像加工ソフトでは結局自前でなんとかしなければなりません。PostScriptについては、上級編で機会を見て解説したいと思います。



◆今回のプログラムリスト
'---------------------------------------------------- ' "Bad Paint ...1997 Program By KaZuhiro FuRuhata" '---------------------------------------------------- RESOURCES "about.res": ' "リソースファイルを読み込む" '--------------------- "定数"------------------------- _fileMenu = 1: ' "ファイルメニュー" _editMenu = 2: ' "エディット(編集)メニュー" _effectMenu = 3: ' "加工メニュー" _fileOpen = 1: ' "ファイルメニュー:開く" _fileSave = 3: ' "ファイルメニュー:保存" _fileQuit = 5: ' "ファイルメニュー:終了" _upDownEffect = 1: ' "上下反転" _penCursor = 128: ' "ペンカーソルのリソース番号" '----------------- "グローバル変数"------------------- DIM cport& gRowBytes% = 0: ' "オフスクリーンのrowBytes" gGRAM& = 0: ' "オフスクリーンのアドレス" gImageX = 320: ' "画像の横の長さ" gImageY = 240: ' "画像の縦の長さ" gOffScreen& = 0: ' "0の時は確保されていない!" gR = 0: '"赤色" gG = 0: '"緑色" gB = 0: '"青色" gQuit_flag = _false: '"終了フラグ" END GLOBALS: ' "グローバル変数定義の終了宣言" '----------------------------------------------- ' "オフスクリーンのrowBytesを求める" '----------------------------------------------- CLEAR LOCAL LOCAL FN getRowBytes PixMapH& = FN GETGWORLDPIXMAP(gOffScreen&): ' "オフスクリーンの画像ハンドルを求める" err% = FN LOCKPIXELS(PixMapH&): ' "画像ハンドルをロック!" LONG IF err% gGRAM& = FN GETPIXBASEADDR(PixMapH&): ' "画像が格納されている先頭のアドレスを求める" gRowBytes% = {[PixMapH&] + _rowBytes} AND &H3FFF:' "rowBytesを求める" END IF END FN ' ----------------------------------------------- ' "オフスクリーンを確保する" ' gOffScreen& = "オフスクリーンのアドレス" ' ----------------------------------------------- CLEAR LOCAL LOCAL FN setOffscreen DIM rect;8 LONG IF gOffScreen& > 0 CALL DISPOSEGWORLD(gOffScreen&): ' "オフスクリーンを破棄" WINDOW CLOSE #1: ' "ウィンドウを閉じて、新しいウィンドウを開く" WINDOW #1,"Image Effecter",(16,45)-(16+gImageX,45+gImageY),_docNoGrow END IF CALL SETRECT(rect,0,0,gImageX,gImageY): '"320x240の画面を作成" err% = FN NEWGWORLD(gOffScreen&,32,rect,0,0,0):' "オフスクリーンを確保する" LONG IF err% BEEP: ' "本当はアラートを出してメモリ不足の旨をユーザーに知らせるべきです" BEEP ' "リソースエディタで128番のアラートでも作ってerr% = FN ALERT(128,0)とでもしましょう" END: ' "多くの場合、メモリ不足" END IF FN getRowBytes: ' "rowBytesを求める" END FN '------------------------------------------------------------- ' "PICTファイルをオープンしてオフスクリーンに描画する" '------------------------------------------------------------- CLEAR LOCAL LOCAL FN openPictFile DIM rect;8 f$ = FILES$(_fOpen,"PICT",,vRefNum%): ' "ファイル選択ダイアログの表示" LONG IF f$<>"" OPEN "I",#1, f$,,vRefNum%: ' "PICTファイルオープン" fileSize& = LOF(1,1): ' "ファイルサイズを求める" pictHandle& = FN NEWHANDLE(fileSize&+4) LONG IF pictHandle& err = FN HLOCK(pictHandle&): ' "PICTハンドルをロック!" LONG IF err = 0 READ FILE#1, [pictHandle&], fileSize&: ' "ファイルサイズ分だけファイルから読み込む" BLOCKMOVE [pictHandle&]+512,[pictHandle&],fileSize& - 512:' "先頭512バイトを消す" err = FN HUNLOCK(pictHandle&): ' "ハンドルロック解除" err = FN SETHANDLESIZE(pictHandle&, fileSize&-512):' "メモリサイズを512減らす" err = FN HLOCK(pictHandle&): ' "ハンドルをロック!" rect;8 = [pictHandle&]+_picFrame: ' "PICTの画像の矩形を取り出す" gImageX = rect.right: ' "右側の座標を取り出す" gImageY = rect.bottom: ' "下側の座標を取り出す" '---------------------------------------------------- FN setOffscreen: ' "オフスクリーンを確保する!" CALL SETGWORLD(gOffScreen&,0): '"オフスクリーンに切り替える" CALL DRAWPICTURE(pictHandle&,rect) CALL SETGWORLD(cport&,0): '"ウィンドウに切り替える" '---------------------------------------------------- err = FN HUNLOCK(pictHandle&) END IF err = FN DISPOSHANDLE(pictHandle&): ' "PICTハンドルを破棄" XELSE BEEP: ' "ハンドルが確保できない〜エラーいこっちゃ" END IF CLOSE #1: ' "ファイルを閉じる" END IF END FN '-------------------------------- ' "オフスクリーンからウィンドウへ転送" '-------------------------------- CLEAR LOCAL LOCAL FN transfer DIM rect;8 LONG IF gOffScreen& > 0 CALL SETRECT(rect,0,0,gImageX,gImageY): ' "転送サイズを設定" CALL COPYBITS(#gOffScreen&+2,#cport&+2,rect,rect,_srcCopy,0):' "オフスクリーンからウィンドウに転送!" END IF END FN '----------------------------------------------- ' "オフスクリーンのカラーを読み出す" '----------------------------------------------- CLEAR LOCAL LOCAL FN myPOINT(x,y) IF (x<0) OR (x>=gImageX) OR (y<0) OR (y>=gImageY) THEN EXIT FN LONG IF gRowBytes% > 0 adrs& = gGRAM& + y*gRowBytes% + x*4: '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している" gR = PEEK(adrs&+1): ' "32ビットオフスクリーンはaRGB順に並んでいる。" gG = PEEK(adrs&+2) gB = PEEK(adrs&+3) END IF END FN '----------------------------------------------- ' "オフスクリーンに点を表示する" '----------------------------------------------- CLEAR LOCAL LOCAL FN myPSET(x,y) IF (x<0) OR (x>=gImageX) OR (y<0) OR (y>=gImageY) THEN EXIT FN LONG IF gRowBytes% > 0 adrs& = gGRAM& + y*gRowBytes% + x*4: '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している" POKE adrs&,0 : '"未使用領域(フォトショップ等ではαチャンネル保存用として使用される事もある)" POKE adrs&+1,gR: ' "32ビットオフスクリーンはaRGB順に並んでいる。" POKE adrs&+2,gG POKE adrs&+3,gB END IF END FN '-------------------------------------------------------- ' "自前で画面を消去する" ' "勉強用なので、非常に低速な方法でやってます" '-------------------------------------------------------- CLEAR LOCAL LOCAL FN myCLS gR = 255 gG = 255 gB = 255 FOR y = 0 TO gImageY-1 FOR x = 0 TO gImageX-1 FN myPSET(x,y) NEXT x NEXT y END FN '-------------------------------------------------------- ' "Pict画像の保存" '-------------------------------------------------------- CLEAR LOCAL LOCAL FN savePict DIM header%(256), rect;8 DEF OPEN "PICT": ' "ファイルタイプをPICTにする" saveFile$ = FILES$(_fSave,"保存ファイル名:","名称未設定",volRefNum%) LONG IF LEN(saveFile$): '"ファイル名の長さが1以上、つまりファイル名が入力された場合" OPEN "O",#1,saveFile$,,volRefNum%: '"保存するファイル名で新規に開く" CALL SETGWORLD(gOffScreen&,0): ' "描画側をオフスクリーン側にします" CALL SETRECT(rect,0,0,gImageX,gImageY): ' "保存するサイズを設定する" savePicture& = FN OPENPICTURE(rect): '"ピクチャーハンドルを作成し、記録開始" CALL COPYBITS(#gOffScreen&+2,#gOffScreen&+2,rect,rect,_srcCopy,0):'"自分自身に書き込む(お約束)" CALL CLOSEPICTURE: '"記録終了" CALL SETGWORLD(cport&,0): ' "描画側を元に戻します" WRITE FILE #1,@header%(0),512: ' "512バイトの空ヘッダーを書き込む" bytes& = FN GETHANDLESIZE(savePicture&): '"ピクチャーハンドルのサイズを求める(Toolbox)" err% = FN HLOCK(savePicture&): '"ハンドルロック" WRITE FILE #1,[savePicture&],bytes&: '"まとめて一気に書き込む" CLOSE #1: '"ファイルを閉じます" CALL KILLPICTURE(savePicture&): '"ピクチャーハンドルは、もういらないので抹殺" END IF END FN '=============================================== ' "上下反転" '=============================================== CLEAR LOCAL LOCAL FN upDownReverse LONG IF gImageY > 1 FOR y = 0 TO (gImageY-1)/2: ' "半分までやれば上下が反転します" FOR x = 0 TO gImageX-1 FN myPOINT(x,y) saveR = gR saveG = gG saveB = gB FN myPOINT(x,(gImageY-1)-y) FN myPSET(x,y) SWAP saveR,gR: ' "入れ替える変数の型(整数、文字)は同じでないと駄目です" SWAP saveG,gG SWAP saveB,gB FN myPSET(x,(gImageY-1)-y) NEXT NEXT END IF BEEP: ' "加工が終了したことをビープ音で知らせる!" FN transfer: ' "できあがった画像を転送する" END FN '-------------------------------------------------------- ' "マウスイベントを処理する" '-------------------------------------------------------- CLEAR LOCAL LOCAL FN drawPoint DIM myPoint.4: ' "マウス位置を取得するためのポイントレコードを用意" CALL GETMOUSE(myPoint): ' "マウス位置を取得(ローカル座標になります)" gR = 255: ' "カラーはとりあえず赤色に設定" gG = 0 gB = 0 x% = myPoint.h%: ' "ウィンドウ上でのマウスのX座標" y% = myPoint.v%: ' "ウィンドウ上でのマウスのY座標" LONG IF FN BUTTON FN myPSET(x%,y%): ' "点を打つ" FN transfer END IF ' "カーソル位置がウィンドウ内かどうか調べる" LONG IF ( (x% >=0 ) AND (x% < gImageX) ) AND ( (y% >= 0 ) AND (y% < gImageY) ) CURSOR _penCursor: ' "ペンカーソルに変更" XELSE CURSOR _arrowCursor: ' "通常の矢印カーソルに変更" END IF END FN '-------------------------------------------------------- ' "アップデートなどのイベントを取得する" '-------------------------------------------------------- CLEAR LOCAL LOCAL FN doDialog evnt = DIALOG(0) id = DIALOG(evnt): '"発生したイベントの種類" SELECT evnt CASE _wndRefresh: '"ウィンドウリフレッシュ(アップデートイベント)" FN transfer: '"アップデートイベントなので画面を再描画がする" END SELECT END FN '-------------------------------------------------------- ' "アバウト画面の表示" '-------------------------------------------------------- CLEAR LOCAL LOCAL FN about err = FN ALERT(128,0) END FN '-------------------------------------------------------- ' "メニューを構築する" '-------------------------------------------------------- CLEAR LOCAL LOCAL FN initMenu APPLE MENU "About Bad Paint..." '"ファイルメニュー" MENU _fileMenu,0,_enable,"ファイル" MENU _fileMenu,_fileOpen,_enable,"/O開く..." MENU _fileMenu,2,_enable,";" MENU _fileMenu,_fileSave,_enable,"/S名前を付けて保存..." MENU _fileMenu,4,_enable,";" MENU _fileMenu,_fileQuit,_enable,"/Q終 了" ' "クリップボード等のコピー&ペーストを行う場合は EDIT MENU 2 とします。 MENU _editMenu,0,_disable,"編集" MENU _editMenu,1,_disable,"取り消し" MENU _editMenu,2,_disable,";" MENU _editMenu,3,_disable,"/Xカット" MENU _editMenu,4,_disable,"/Cコピー" MENU _editMenu,5,_disable,"/Vペースト" MENU _editMenu,6,_disable,"消去" MENU _editMenu,7,_disable,";" MENU _editMenu,8,_disable,"/A全てを選択" ' "加工メニュー" MENU _effectMenu,0,_enable,"加 工" MENU _effectMenu,_upDownEffect,_enable,"上下反転" END FN '--------------------------------------------- ' "メニューの選択" '--------------------------------------------- CLEAR LOCAL LOCAL FN doMenus menuID = MENU(_menuID): '"選択されたメニューバー項目の番号" itemID = MENU(_itemID): '"プルダウンメニューで選択された項目番号" SELECT menuID CASE _appleMenu: ' "アバウト画面の表示(_appleMenuはあらかじめ定義されています)" FN about CASE _fileMenu : ' "ファイルメニュー" SELECT itemID CASE _fileOpen: ' "画像を読み込む(開く)" FN openPictFile CASE _fileSave: ' "画像の保存" FN savePict CASE _fileQuit: ' "終了が選択された" gQuit_flag = _true END SELECT CASE _effectMenu: ' "加工メニュー" SELECT itemID CASE _upDownEffect: FN upDownReverse: ' "加工メニューの上下反転が選択された" END SELECT END SELECT MENU: ' "これがないとメニューバーの項目が強調表示されたままになってしまいます" END FN WINDOW OFF WINDOW #1,"Bad Paint",(0,0)-(gImageX, gImageY),_docNoGrow CALL GETPORT(cport&): ' "ウィンドウのグラフポートを確保しておきます" ON MENU FN doMenus: '"メニューが選択された時の飛び先" ON DIALOG FN doDialog: '"ダイアログイベントが発生した時の飛び先" FN initMenu: '"メニューの初期化" FN setOffscreen: '"オフスクリーンの確保" FN myCLS: '"画面を消去します" FN transfer: '"オフスクリーンからウィンドウへ画像を転送" DO FN drawPoint: ' "マウスによる描画" HANDLEEVENTS: ' "イベント処理は自動(こりゃ楽ですな)" UNTIL gQuit_flag CALL DISPOSEGWORLD(gOffScreen&): ' "オフスクリーンの破棄(関数にしてもいいですね)" END