プログラム講座 中級編13

- お絵かきソフトを作ろう(その2) -
QuickDrawの使い方

 中級編13です。今回は中級編12の続きです。今回はQuickDrawを使用してペンによる描画を行います。今までほとんどQuickDrawの描画については説明していませんでした。今回の講座では、まずペンによる描画について説明します。




◆QuickDraw(クイックドロー)とは?
 Macでグラフィック関係のプログラムを作成しようとすると必ず耳にする(目にする?)言葉があります。それがQuickDrawです。これは読んで字のごとし「描画全般」を扱う関数群を示しています。要するにMacで何か描画したいと思ったらQuickDrawを使えばよいわけです。QuickDrawは以下のような図形/処理等を扱うことが出来ます。
 見ての通り通常の図形は描画できますが、例えばベジエ曲線やスプライン曲線は自前で作成しなければなりません。特殊効果も、転送モードでできなくはありませんが、フォトショップのようなエフェクトは自前で作成しなければなりません。左右反転や回転も自前で用意しなければなりません。また、点を表示する関数は用意されていませんので、点を表示する場合はラインで代用します。

 Mac発表当時は上記機能でも十分でしたが、さすがに10年以上経過した現在では役不足です。後にQuickDraw GXが発表されますが、諸々の都合で消滅しました。Rhapsodyが出るまでは、とりあえずQuickDrawを使うしかありません。

注意
QuickDrawは数学座標系で演算処理を行います。従来は(0,0)-(0,50)にラインを描画しなさいと命令すれば線を引くことが出来ましたが、QuickDrawでは何も表示されません。つまり太さ0の線とみなします。



◆切れ目のなく点を表示させるには?
 前回はマウスボタンが押されている間、点を表示するようにプログラムしましたが、実際にマウスを移動させてみると点がとびとびになって表示されてしまいます。これはマウスの追従速度を上げても(割り込み速度を上げても)無理です。このような場合、どうすればよいかというと「前回の座標と現在の座標をラインで描画する」と解決できます。
 実際のプログラムは以下のようになります。
  LONG IF FN BUTTON
    CALL SETGWORLD(gOffScreen&,0):                ' "描画側をオフスクリーン側にします"
    CALL LINETO(x%,y%)
    CALL SETGWORLD(cport&,0):                     ' "描画側を元に戻します"
    FN transfer
  XELSE
    CALL SETGWORLD(gOffScreen&,0):                ' "描画側をオフスクリーン側にします"
    CALL MOVETO(x%,y%):                           ' "ペン位置を移動させます"
    CALL SETGWORLD(cport&,0):                     ' "描画側を元に戻します"
  END IF
 ラインを描画するには「CALL LINETO(X座標,Y座標)」とします。この時必ず描画前に描画すべきオフスクリーンにグラフポートが設定されていなければなりません。そうしないと、どこか不明な所に描画してしまいます。描画後は元のグラフポートに戻しておく必要があります。これを行うのがSETGWORLDです。これだけ気を付ければ簡単に描画させる事ができます。



◆ペンサイズとペンモード
 中級編12ではペンの太さは同じでしたが、今度はメニューからペンサイズとペンモードを選択できるように改良しましょう。ペンの太さを変更するメニューは従来のメニューの作り方と変わりません。メニューが多くなってきたら(本当はできるだけメニューリソースで)リソースで用意しておくべきでしょう。リソースで用意しておけば、メニューを日本語から英語、フランス語など割と手軽に他国語に対応させる事ができます。
 さて、ペンサイズを変更するにはCALL PENSIZEを使用します。ペンサイズは最大16x16までで縦横個別に設定することが出来ます。
 ペンモードを変更するにはCALL PENMODEを使用します。ペンモードについてはハンドブックマニュアルを参照してください。例によってマニュアルには、どういう場合に、どのモードを使用すればよいのかは書いてありません。使い方については、また別の機会に説明します。

 これでペンサイズとペンモードを自由に変更する事ができるようになりました。



◆メニューにチェックマークを付けるには?
 フォントメニュー等、メニューの中で1つしか選択できないものには「チェックマーク」がついています。今回追加したペンサイズとペンモードは複数選択する事ができませんし、現在のペンサイズ、ペンモードが何なのかをユーザーにわかるようにした方が親切です。
 幸いにしてFuture BASICでは指定メニューの指定項目に1つだけチェックマークを付けるという便利な命令があります。それがDEF CHECKONEITEMです。この命令の書式は以下のようになっています。

DEF CHECKONEITEM(メニューバー番号,チェックマークを付けるメニュー項目番号)

 この命令を使えばメニューにチェックマークを付けるのは簡単です。



◆終わりに
 ペンサイズとペンモードが選択できるようになりましたが、まだまだ課題はたくさんあります。なんといっても、黒色でしか描画できない事とアンドゥ(取り消し)が使えない事です。次回は、カラーを選択できるようにし、さらにアンドゥ機能も付加する事にしましょう。



◆今回のプログラムリスト
'---------------------------------------------------- ' "Bad Paint ...1997 Program By KaZuhiro FuRuhata" '---------------------------------------------------- RESOURCES "about.res": ' "リソースファイルを読み込む" '--------------------- "定数"------------------------- _fileMenu = 1: ' "ファイルメニュー" _editMenu = 2: ' "エディット(編集)メニュー" _effectMenu = 3: ' "加工メニュー" _penSizeMenu = 4: ' "ペンサイズメニュー" _penModeMenu = 5: ' "ペンモード" _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: '"終了フラグ" penSize% = 0: ' "ペンサイズ" penMode% = _patCopy: ' "ダイレクトコピーモード" 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 CALL SETGWORLD(gOffScreen&,0): ' "描画側をオフスクリーン側にします" CALL LINETO(x%,y%) CALL SETGWORLD(cport&,0): ' "描画側を元に戻します" FN transfer XELSE CALL SETGWORLD(gOffScreen&,0): ' "描画側をオフスクリーン側にします" CALL MOVETO(x%,y%): ' "ペン位置を移動させます" CALL SETGWORLD(cport&,0): ' "描画側を元に戻します" 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 setPenMode(theMode%) CALL SETGWORLD(gOffScreen&,0): ' "描画側をオフスクリーン側にします" SELECT theMode% CASE 1: CALL PENMODE(_patCopy) CASE 2: CALL PENMODE(_patOr) CASE 3: CALL PENMODE(_patXor) CASE 4: CALL PENMODE(_patBic) CASE 5: CALL PENMODE(_notPatCopy) CASE 6: CALL PENMODE(_notPatOr) CASE 7: CALL PENMODE(_notPatXor) CASE 8: CALL PENMODE(_notPatBic) END SELECT CALL SETGWORLD(cport&,0): ' "描画側を元に戻します" 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,"上下反転" ' "ペンサイズメニュー" MENU _penSizeMenu,0,_enable,"ペンサイズ" MENU _penSizeMenu,1,_enable,"1" MENU _penSizeMenu,2,_enable,"2" MENU _penSizeMenu,3,_enable,"3" MENU _penSizeMenu,4,_enable,"4" MENU _penSizeMenu,5,_enable,"5" MENU _penSizeMenu,6,_enable,"6" MENU _penSizeMenu,7,_enable,"7" MENU _penSizeMenu,8,_enable,"8" MENU _penSizeMenu,9,_enable,"9" MENU _penSizeMenu,10,_enable,"10" MENU _penSizeMenu,11,_enable,"11" MENU _penSizeMenu,12,_enable,"12" MENU _penSizeMenu,13,_enable,"13" MENU _penSizeMenu,14,_enable,"14" MENU _penSizeMenu,15,_enable,"15" MENU _penSizeMenu,16,_enable,"16" ' "ペンモードメニュー" MENU _penModeMenu,0,_enable,"ペンモード" MENU _penModeMenu,1,_enable,"Copy" MENU _penModeMenu,2,_enable,"Or" MENU _penModeMenu,3,_enable,"Xor" MENU _penModeMenu,4,_enable,"Bic" MENU _penModeMenu,5,_enable,"NotCopy" MENU _penModeMenu,6,_enable,"NotOr" MENU _penModeMenu,7,_enable,"NotXor" MENU _penModeMenu,8,_enable,"NotBic" DEF CHECKONEITEM(_penSizeMenu,1): ' "ペンサイズの初期値は1" DEF CHECKONEITEM(_penModeMenu,1): ' "COPYモード" 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 CASE _penSizeMenu: ' "ペンサイズメニュー" penSize% = itemID: ' "ペンサイズを設定" CALL SETGWORLD(gOffScreen&,0): ' "描画側をオフスクリーン側にします" CALL PENSIZE(penSize%,penSize%): ' "ペンサイズを設定" CALL SETGWORLD(cport&,0): ' "描画側を元に戻します" DEF CHECKONEITEM(_penSizeMenu,itemID): ' "ペンサイズにチェックマーク" CASE _penModeMenu: ' "ペンモードメニュー" FN setPenMode(itemID): ' "ペンモードを設定します" DEF CHECKONEITEM(_penModeMenu,itemID): ' "ペンモードにチェックマーク" 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