プログラム講座 中級編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