プログラム講座 中級編7
- 画像加工アプリケーションを作成する -
 中級編7です。今回は中級編のまとめという事で、今まで作ってきた関数をまとめて「画像加工」アプリケーションを作成してみます。
◆今回のプログラム
 今回新しく作成する部分は画像を加工する部分です。オフスクリーンの画像を直接操作するというのは前回やりました。画像加工は結果が目に見えるので作っていて楽しいものです。しかし、Macでとなると話は別でいろいろ知らなければならない事があります。この中級編7を読んでいる方はすでに、画像加工アプリケーションを作成するだけの事はできるはずです。
 今回のプログラムは、各自でいろいろな加工ルーチンを作成してもらうため、「上下反転」のフィルタしか用意しません。
◆設計の重要さ
 今回は今までのプログラムをくっつける(コピー&ペースト)するだけでできてしまいます。そこで、今までの講座ではほとんど触れられていない「設計の重要さ」について書きたいと思います。
 今回のプログラムは「コピー&ペーストしてくっつけていく」程度でできあがりました。従来の形式のBASICでは、このような事は得意ではありませんでした。なぜ、コピー&ペーストができるかというと「関数が独立」しているためです。簡単に言うと「グローバル変数を使用しない」という事になります。
 全ての関数が必要であった場合、どの関数も必要なため他のアプリケーションにコピーして使うという事が難しくなってしまいます。
◆使い回し(ライブラリ)
 関数をグローバル変数に依存しないようにしておけば「使い回し」をする事ができます。さらに、動作は保証されているわけですから、バグが発生しても新しく作成した部分だけテストすればよいので楽ですし開発時間も短縮できます。このように、自前のプログラムをためておき「ライブラリ」にしておくと、さらに開発効率は良くなります。
 ライブラリをためるのが面倒だという場合が多いでしょう。幸いFBには標準でそのようなサンプル(関数)がいくつか用意されていますので、それを使うようにすればよいでしょう。また、素早くプログラムを作成したい人はProgram Generatorを使用するのもいいでしょう。ただし、PGのマニュアルは、もっとなんとかならないものかと思いますがf(-.-b
◆プログラム設計
 このプログラムは一体どういう動作をし、何を求めるのか。これが基本設計の根幹の部分です。これを元に関数を作成していきます。プログラムの設計というとフローチャートやPADなどがありますが、特にそのようなものを覚える必要はありません。それが中規模であろうと大規模であろうと不要でしょう。
 どういう具合にしプログラムを作ったらよいか分からない、という人が結構多いかもしれません。まず作るべきものがない時は、当然プログラムする必要はありません。作るべきものができた時、始めてプログラムの必要性がでてきます。
 今回のプログラムであれば、だいたい裏紙にでも以下のように書いておけばいいでしょう。
画像加工ソフト (以下は必要なもの)
- 初期設定
 - 仮想画面の設定
 - 仮想画面から実際の画面への描画
 - 画像を加工するプログラム
 - 画像を保存する部分
 
 これに沿って今度は細かく分けます例えば仮想画面の設定であれば
仮想画面の設定
- 仮想画面の確保
 - 仮想画面の破棄
 - 仮想画面の横幅を求める
 
 これを順番にプログラムしていけばいいのです。あと勘違いしてはいけないのが、最初から全部このように書く事はありません。途中で、これが要りそうだな、と思ったら項目に追加すればいいのです。また、格好よくエディタやワープロなどで出力する必要もないでしょう(会社ではそうはいかないでしょうが。でも大抵の所はフローチャートとかで設計しているのではないでしょうか)。手書きで自分のわかるように書けばOK!です。すでに存在する関数や使い回す部分に関しては、深く書く必要もないので「仮想画面の設定」という一言で十分です。逆にこのようにして、新しい部分に力を入れていきます。
◆基本設計は重要
 小規模なアプリケーションであれば、基本設計が間違っていても作成し直す事ができますが、大規模なプログラムではそうはいきません。小さいプログラムをいくつか作成していくと「プログラムは正常に動作している。だが、使いにくい、機能を追加しにくい」などという事があるでしょう。つまりプログラム以前に「基本設計」がまずいのです。基本設計が良ければ、そのソフトは長く使用に耐えることが出来ます。どんな機能を盛り込むか、バージョンアップはどうするのか、などなど作っていくうちに「こうしてはいけない」「こうした方が結果的に良い」という事がわかってくると思います。
 ちなみに基本設計がしっかりしていないと、つぎはぎプログラムになってしまい機能追加もバグフィックスも難しくなって、まさに「泥沼」になってしまいます。では、「泥沼」になってしまった時どうするか? これには大変良い解決策があります。それは「作りなおす事」です。つまり基本設計から見直して作成した方が結果的によいものができますし、時間的にも速く完成できます。
◆終わりに
 プログラムを作成していると、わからない部分が必ずでてきます。そのような状況に陥ってしまったら、NiftyServeであればFMACPROなどで質問すると良いでしょう。もちろん、ある程度の状況とプログラムの動作などを書いて発言しなければ駄目です。あとはまわりに仲間(ネットワーク)を作っておく事です。自分がアドバイスできるようになれば初級プログラマを脱して見事中級プログラマになったと思ってよいのではないでしょうか。
◆今回のプログラムリスト
'-----------------------------------------------
' "仮想画面(オフスクリーン)のrowBytesを求めて直接描画する"
'-----------------------------------------------
DIM offScreen&,cport&,rect;8
DIM header%(256)
rowBytes% = 0:                                    ' "オフスクリーンのrowBytes"
GRAM& = 0:                                        ' "オフスクリーンのアドレス"
ImageX = 320:                                     ' "画像の横の長さ"
ImageY = 240:                                     ' "画像の縦の長さ"
offScreen& = 0:                                   ' "0の時は確保されていない!"
R = 0
G = 0
B = 0
END GLOBALS
'-----------------------------------------------
' "オフスクリーンのrowBytesを求める"
'-----------------------------------------------
LOCAL FN getRowBytes
  PixMapH& = FN GETGWORLDPIXMAP(offScreen&)
  err% = FN LOCKPIXELS(PixMapH&)
  LONG IF err%
    GRAM& = FN GETPIXBASEADDR(PixMapH&)
    rowBytes% = {[PixMapH&] + _rowBytes} AND &H3FFF
  END IF
END FN
' -----------------------------------------------
'  "オフスクリーンを確保する"
' offScreen& = "オフスクリーンのアドレス"
' -----------------------------------------------
LOCAL FN setOffscreen
  LONG IF offScreen& > 0
    CALL DISPOSEGWORLD(offScreen&)
    WINDOW CLOSE #1
    WINDOW #1,"Image Effecter",(16,45)-(16+ImageX,45+ImageY),_docNoGrow
  END IF
  CALL SETRECT(rect,0,0,ImageX,ImageY):           '"320x240の画面を作成"
  err% = FN NEWGWORLD(offScreen&,32,rect,0,0,0)
  LONG IF err%
    BEEP
    BEEP
    END:                                          ' "多くの場合、メモリ不足"
  END IF
  FN getRowBytes:                                 ' "rowBytesを求める"
END FN
'-------------------------------------------------------------
' "PICTファイルをオープンしてオフスクリーンに描画する"
'-------------------------------------------------------------
LOCAL FN openPictFile
  DIM rectPICT;8
  
  f$ = FILES$(_fOpen,"PICT",,vRefNum%)
  LONG IF f$<>""
    OPEN "I",#1, f$,,vRefNum%
    fileSize& = LOF(1,1)
    pictHandle& = FN NEWHANDLE(fileSize&+4)
    LONG IF pictHandle&
      err = FN HLOCK(pictHandle&)
      LONG IF err = 0
        READ FILE#1, [pictHandle&], fileSize&
        BLOCKMOVE [pictHandle&]+512,[pictHandle&],fileSize& - 512
        err = FN HUNLOCK(pictHandle&)
        err = FN SETHANDLESIZE(pictHandle&, fileSize&-512)
        err = FN HLOCK(pictHandle&)
        rectPICT;8 = [pictHandle&]+_picFrame
        ImageX = rectPICT.right
        ImageY = rectPICT.bottom
        '----------------------------------------------------
        FN setOffscreen:                          ' "オフスクリーンを確保する!"
        CALL SETGWORLD(offScreen&,0):             '"オフスクリーンに切り替える"
        CALL DRAWPICTURE(pictHandle&,rectPICT)
        CALL SETGWORLD(cport&,0):                 '"ウィンドウに切り替える"
        '----------------------------------------------------
        err = FN HUNLOCK(pictHandle&)
      END IF
      err = FN DISPOSHANDLE(pictHandle&)
    XELSE 
      BEEP
    END IF
    CLOSE #1
  END IF
END FN
'--------------------------------
' Copy Offscreen -> Window
'--------------------------------
CLEAR LOCAL
LOCAL FN transfer
  LONG IF offScreen& > 0
    CALL SETRECT(rect,0,0,ImageX,ImageY)
    CALL COPYBITS(#offScreen&+2,#cport&+2,rect,rect,_srcCopy,0)
  END IF
END FN
'-----------------------------------------------
' "オフスクリーンのカラーを読み出す"
'-----------------------------------------------
LOCAL FN myPOINT(x,y)
  LONG IF rowBytes% > 0
    adrs& = GRAM& + y*rowBytes% + x*4:            '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している"
    R = PEEK(adrs&+1):                            ' "32ビットオフスクリーンはaRGB順に並んでいる。"
    G = PEEK(adrs&+2)
    B = PEEK(adrs&+3)
  END IF
END FN
'-----------------------------------------------
' "オフスクリーンに点を表示する"
'-----------------------------------------------
LOCAL FN myPSET(x,y)
  LONG IF rowBytes% > 0
    adrs& = GRAM& + y*rowBytes% + x*4:            '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している"
    POKE adrs&,0 :                                '"未使用領域(フォトショップ等ではαチャンネル保存用として使用される事もある)"
    POKE adrs&+1,R:                               ' "32ビットオフスクリーンはaRGB順に並んでいる。"
    POKE adrs&+2,G
    POKE adrs&+3,B
  END IF
END FN
'--------------------------------------------------------
' "自前で画面を消去する"
'--------------------------------------------------------
LOCAL FN myCLS
  R = 255
  G = 255
  B = 255
  FOR y = 0 TO ImageY-1
    FOR x = 0 TO ImageX-1
      FN myPSET(x,y)
    NEXT x
  NEXT y
  
END FN
'===============================================
'   "今回作成した部分(上下反転)"
'===============================================
LOCAL FN upDownReverse
  LONG IF ImageY > 1
    FOR y = 0 TO (ImageY-1)/2
      FOR x = 0 TO ImageX-1
        FN myPOINT(x,y)
        saveR = R
        saveG = G
        saveB = B
        FN myPOINT(x,(ImageY-1)-y)
        FN myPSET(x,y)
        SWAP saveR,R
        SWAP saveG,G
        SWAP saveB,B
        FN myPSET(x,(ImageY-1)-y)
      NEXT
    NEXT
  END IF
  BEEP:                                           ' "加工が終了したことをビープ音で知らせる!"
  FN transfer:                                    ' "できあがった画像を転送する"
END FN
'--------------------------------------------------------
'   "Pict画像の保存"
'--------------------------------------------------------
LOCAL FN savePict
  DEF OPEN "PICT":                                ' "ファイルタイプをPICTにする"
  CALL SETRECT(rect,0,0,ImageX,ImageY)
  saveFile$ = FILES$(_fSave,"保存ファイル名:","名称未設定",volRefNum%)
  LONG IF LEN(saveFile$)
    OPEN "O",#1,saveFile$,,volRefNum%
    
    CALL SETGWORLD(offScreen&,0):                 ' "描画側をオフスクリーン側に"
    savePicture& = FN OPENPICTURE(rect)
    COLOR _zWhite
    CALL COPYBITS(#offScreen&+2,#offScreen&+2,rect,rect,_srcCopy,0)
    CALL CLOSEPICTURE
    CALL SETGWORLD(cport&,0):                     ' "描画側を元に戻す"
    
    WRITE FILE #1,@header%(0),512:                ' "Header Write"
    bytes& = FN GETHANDLESIZE(savePicture&)
    err% = FN HLOCK(savePicture&)
    WRITE FILE #1,[savePicture&],bytes&
    CLOSE #1
    CALL KILLPICTURE(savePicture&)
  END IF
END FN
'--------------------------------------------------------
' "アップデートなどのイベントを取得する"
'--------------------------------------------------------
LOCAL FN doDialog
  evnt = DIALOG(0)
  id = DIALOG(evnt)
  SELECT evnt
    CASE _wndRefresh
      FN transfer:                                '"アップデートイベントなので画面を再描画がする"
  END SELECT
END FN
'--------------------------------------------------------
' "メニューを構築する"
'--------------------------------------------------------
LOCAL FN initMenu
  'File Menu
  MENU 1,0,_enable,"ファイル"
  MENU 1,1,_enable,"/O開く..."
  MENU 1,2,_enable,";"
  MENU 1,3,_enable,"/S名前を付けて保存..."
  MENU 1,4,_enable,";"
  MENU 1,5,_enable,"/Q終 了"
  
  MENU 2,0,_enable,"加 工"
  MENU 2,1,_enable,"上下反転"
END FN
'---------------------------------------------
' "メニューの選択"
'---------------------------------------------
LOCAL FN doMenus
  menuID = MENU(_menuID)
  itemID = MENU(_itemID)
  
  SELECT menuID
    CASE 1 :                                      ' File Menu
      SELECT itemID
        CASE 1:
          FN openPictFile
        CASE 3:
          FN savePict
        CASE 5:                                   ' Quit...
          CALL DISPOSEGWORLD(offScreen&)
          END
      END SELECT
    CASE 2:
      SELECT itemID
        CASE 1:
          FN upDownReverse:                       ' "加工メニューの上下反転"
      END SELECT
  END SELECT
  MENU
END FN
WINDOW OFF
WINDOW #1,"Image Effecter",(16,45)-(16+320,45+240),_docNoGrow
CALL GETPORT(cport&):                             ' "ウィンドウのグラフポートを確保しておきます"
ON MENU FN doMenus
ON DIALOG FN doDialog
FN initMenu
FN setOffscreen
FN myCLS:                                         '"画面を消去する"
FN transfer
DO
  HANDLEEVENTS
UNTIL theProgramEnds
END