プログラム講座 上級編9

- 汎用関数の作成[その1] -

 上級編9です。今回から数回に渡って「汎用関数」を作成します。汎用と言っても、ほとんどはToolboxに用意されているもので事足ります。しかし、Toolboxの制限により困った事が発生する事もあります。例えばテキストエディットの32KBの制限などは良い例でしょう。また、元が米国のマシンなので日本語処理を行おうとすると内部的に面倒な処理を行わなければなりません。




◆ファイルの分割方法について
 作成するプログラムが巨大になっていくと1ファイルに全ての処理を記述していたのでは、リストを眺めるだけでも大変です。また、些細なミスで正常に動作しているプログラムまでおかしくしてしまう事があります。プログラムサイズが巨大化していきそうな場合や今回のような汎用関数を作成する場合、ファイルを分割して作成する事ができます。
 Future BASICでは1.0xとIIでは使用できるファイルが異なります。今回は以下のように分割してあります。これは1.0xでもIIでも、どちらでも大丈夫です。

 各ファイルは「ファイル名に制限」があります。Macでは馴染みが薄いのですがそれぞれのファイル名の最後に「拡張子」といって、どういう種類のファイルか認識させるための名前が必要です。これは上記の(〜)内の英字が、それに該当します。例えばtestというアプリを作成する場合のグローバルファイルは

test.GLBL

 となります。ファイル名の最後が.GLBLであれば特にtestでなくとも構いませんが、やはり作成するアプリの名称を付けておくべきでしょう。その方がメンテナンスもやりやすくなります。



◆インクルードファイルについて
 インクルードファイルには、関数定義のみ記述する規則になっています。インクルードファイル内に別のインクルードファイルを入れる事も可能です。
 ただ単純に関数を記述しファイル名の末尾を.INCLにしただけでは、Future BASICは正しくコンパイルする事ができません。正しくコンパイルさせるためには、以下のようにプログラムを記述しなければなりません。

INCLUDE FILE _aplIncl
GLOBALS "グローバルファイル名"
END GLOBALS

 グローバルファイル名は〜.GLBLになります。この3命令を入れておけばFuture BASICはインクルードファイルとして認識してくれます。ただし、実行及びビルドする場合は〜.MAINファイルのウィンドウを一番手前(カレント)にしてください。インクルードファイルでは単純にコンパイルするだけで実行もビルドも行われません。コンパイルがうまくいかない場合は、インクルードファイルでビルドまたはRUNを実行した後でMAINファイル上で実行またはビルドします。
 またグローバルファイルを変更した場合、全てのファイルをコンパイルし直さなければいけませんので注意して下さい。
 うまくファイルを分ける事ができれば中規模程度のプログラムも作成できるようになります。



◆日本語対応版PRINT文を作成する
 上級編でのライブラリ整備は「メールソフト」への布石です。そのため、Toolboxに頼らずにほとんど自前で処理を行う日本語処理関数やフィルタを作成します。今回は一番基本となる日本語の表示命令、つまりPRINT文を作成します。
 まず、日本語処理で問題となるのが「2バイト処理」です。通常のテキストは内部ではShift-JISコードとなっており1バイト文字(英数字)と2バイト文字(日本語)が混在しています。そのため、指定位置の文字が1バイトか2バイトかを調べるにも先頭からチェックしていかなければなりません。またShift-JISからJIS, EUCコードという他の日本語処理コードへの変換やワードラップ字数計算など非常に面倒です。
 そこで処理を簡単にするために一度テキストを読み込み内部で全て2バイトに変換します。日本語は2バイトのままですが英数字など1バイトコードは以下のようになります。

文字1バイトコード内部コード
2&H32&H0032
A&H41&H0041

 このようにすれば、各種処理が簡単になります。



◆作成する汎用関数(今回は日本語表示処理関数)
 今回作成した関数は以下の通りです。

FN conv2byte(address&,address2&,fileSize&)メモリ1からメモリ2にテキストを2バイトに展開する
FN to2byte(openfile$,fileNumber%,openRefNum%)ファイルからテキストファイルを読み込み2バイトに展開する
FN countLine(myHandle&)行数をカウントする
FN getLineAddress(address&,lineNo&)指定行の先頭アドレスを返す
FN getAllLineAddress(textHandle&)全ての行のアドレスを求めハンドルを返す
FN PRINT2(tableH&,type%,lineNo&,xAdrs&,yAdrs&,x1%,y1%,x2%,y2%)禁則処理付きで1行表示する

 作成した関数はグローバル変数と衝突しないようにCLEAR LOCAL MODE命令を使用しています。



◆FN conv2byte
 この関数は指定アドレスの文字データを2バイトに展開するものです。これはto2byte関数の下請けとして使用しています。処理としてはメモリから1バイト読み込み2バイトコードかどうか調べます。2バイトコードであれば次の1バイト、つまり2バイト目を読み込みます。その後メモリに書き込みを行います。これを指定サイズ分行えば処理は完了します。



◆FN to2byte
 指定されたファイル名とリファレンス番号のファイルからテキストデータを読み込みます。読み込まれたテキストサイズを元に2バイト展開後のメモリを確保します。最悪の場合、つまり2バイトコードが0の場合、必要なメモリサイズは元のファイルサイズの2倍になります。ちょっとメモリの無駄遣いな気もしますが、100KBのテキストでも200KBくらいですので、まあ許せる範囲ではないかと思います。



◆FN countLine
 2バイトに展開されたテキストの総行数を求めます。単純にメモリから2バイトづつ読み込み改行コード(13)であればカウンタを増やすだけです。



◆FN getLineAddress
 指定行のアドレスを求めます。2バイト展開後のテキストアドレスから改行コードが見つかるまでサーチし該当行数であれば、アドレスを返します。



◆FN getAllLineAddress
 2バイト展開後のテキストの全ての行数のアドレスを求めます。テキストハンドルから行数のアドレスを求め取得したアドレスのテーブルをハンドルで返します。この関数は表示を高速化するために存在します。あらかじめ行の先頭アドレスを求めておけば各種処理が高速化できます。これは、常套手段ですね(^^)



◆FN PRINT2
 指定範囲の領域にテキストを表示します。ワードラップするかどうかも指定できます。ただし、とりあえずワードラップするだけです。表示後のX,Y座標を返しますが、関数の戻り値ではなく直接変数アドレスを取得して書き換えています。このため引数は変数の値ではなく、変数アドレスを引数としています。
 最後に空白文字を表示していますが、この空白を表示しないと困った事になってしまいます。実際に消してみて実行してみればわかるでしょう。ゴミが残るためです。むぅ〜。



◆終わりに
 全てFuture BASICで記述、処理しているので大変低速です・・・。大きなテキストファイルを読み込むと最初ハングアップしたような状態になりますが、内部に展開している時間がかかりますので、しばらく我慢しましょう(^^;



◆今回のプログラムリスト
■xFile.GLBL
'----------------------------------------------------------- ' "グローバル変数の定義" '-----------------------------------------------------------
■xFile.INCL
INCLUDE FILE _aplIncl: ' "INLCUDEファイル宣言" GLOBALS "xFile.GLBL" END GLOBALS '----------------------------------------------------------- ' "メモリ1からメモリ2にテキストを2バイトに展開する" '----------------------------------------------------------- CLEAR LOCAL MODE LOCAL FN conv2byte(address&,address2&,fileSize&) cnt& = 0 WHILE _true low% = PEEK(address&): ' "1バイト読み込む" LONG IF (low% >= &H80 AND low% <= &HA0) OR (low% >= &HE0 AND low% <= &HFC) high% = PEEK(address&+1): ' "1バイト読み込む" low% = low%*256 + high% address& = address& + 2 cnt& = cnt& + 2 XELSE address& = address& + 1 cnt& = cnt& + 1 END IF POKE WORD address2&,low% address2& = address2& + 2: ' "2バイトアドレスを進める" LONG IF cnt& > fileSize& POKE LONG address2&,0: ' "終了コード" EXIT FN END IF WEND END FN '----------------------------------------------------------- ' "ファイルからテキストファイルを読み込み2バイトに展開する" ' "展開後のハンドルを返す" '----------------------------------------------------------- CLEAR LOCAL MODE LOCAL FN to2byte(openfile$,fileNumber%,openRefNum%) myHandle& = 0: ' "ハンドルをクリア" LONG IF LEN(openfile$) OPEN "I",#fileNumber,openfile$,,openRefNum%: ' "分割元のファイルをオープン" fileSize& = LOF(fileNumber,1): ' "ファイルサイズを求める" myHandle& = FN NEWHANDLE(fileSize&): ' "ファイルサイズ分メモリを確保します!" myHandle2& = FN NEWHANDLE(fileSize& * 2 + 4):' "ファイルサイズ分×2メモリを確保します!" LONG IF myHandle& AND myHandle2&: ' "ハンドルサイズが0の時はメモリ不足で確保できなかった!" err% = FN HLOCK(myHandle&): ' "メモリが移動しないようにハンドルをロック" err% = FN HLOCK(myHandle2&): ' "メモリが移動しないようにハンドルをロック" READ FILE #fileNumber,[myHandle&],fileSize&:' "ファイルを読み込む" address& = [myHandle&]: ' "ポインタを取得" address2& = [myHandle2&]: ' "ポインタを取得" FN conv2byte(address&,address2&,fileSize&):' "メモリに展開" IF myHandle& THEN err% = FN HUNLOCK(myHandle&):err% = FN DISPOSHANDLE(myHandle&):' "ハンドルロックを解除" IF myHandle2& THEN err% = FN HUNLOCK(myHandle2&):' "ハンドルロックを解除" END IF CLOSE #fileNumber: ' "ファイルを閉じる" END IF END FN = myHandle2& '----------------------------------------------------------- ' "行数をカウントする" ' "行数を返す" '----------------------------------------------------------- CLEAR LOCAL MODE LOCAL FN countLine(myHandle&) count& = 0 address& = [myHandle&] theValue% = PEEK WORD(address&) WHILE theValue% <> 0 IF theValue% = 13 THEN count& = count& + 1 address& = address& + 2 theValue% = PEEK WORD(address&) WEND END FN = count& '----------------------------------------------------------- ' "指定行のアドレスを求める" ' "指定行の先頭アドレスを返す" '----------------------------------------------------------- CLEAR LOCAL MODE LOCAL FN getLineAddress(address&,lineNo&) count& = 0 theValue% = PEEK WORD(address&) firstAddress& = address&: ' "先頭アドレス" WHILE theValue% <> 0 address& = address& + 2 LONG IF theValue% = 13 count& = count& + 1 LONG IF count& = lineNo& theValue% = 0 XELSE firstAddress& = address& END IF END IF IF theValue% <> 0 THEN theValue% = PEEK WORD(address&) WEND END FN = firstAddress& '----------------------------------------------------------- ' "全ての行のアドレスを求めハンドルを返す" ' "エラーの場合は、−1を返す" '----------------------------------------------------------- CLEAR LOCAL MODE LOCAL FN getAllLineAddress(textHandle&) length& = FN countLine(textHandle&): ' "行数を求める" myHandle& = FN NEWHANDLE(length&*4): ' "行数×4バイト分メモリを確保します!" LONG IF myHandle& err% = FN HLOCK(textHandle&): ' "ハンドルをロック" textAddress& = [textHandle&]: ' "アドレス設定" err% = FN HLOCK(myHandle&): ' "ハンドルをロック" address& = [myHandle&] + 4: ' "アドレス設定" FOR lineNo& = 1 TO length& adrs& = FN getLineAddress(textAddress&,lineNo&):' "指定行のアドレスを得る" POKE LONG address&,adrs& address& = address& + 4: ' "アドレスは4バイト" NEXT XELSE myHandle& = -1: ' "エラーの場合は-1を返す" END IF END FN = myHandle& '----------------------------------------------------------- ' "禁則処理付きで1行表示する(実際は複数行にまたがる)" ' "表示後自動的に改行する" ' ' "type = _true ならば 改行" ' "type = _false ならば 改行しない '----------------------------------------------------------- CLEAR LOCAL MODE LOCAL FN PRINT2(tableH&,type%,lineNo&,xAdrs&,yAdrs&,x1%,y1%,x2%,y2%) x% = {xAdrs&} y% = {yAdrs&} IF (x% >= x2%) OR (y% >= y2%) THEN EXIT FN: ' "最初から範囲外の場合はリターン" fh% = USR FONTHEIGHT: ' "現在のフォントの高さを求める" err% = FN HLOCK(tableH&) address& = [tableH&] adrs& = PEEK LONG(address& + lineNo& * 4): ' "指定行のアドレスを得る" theValue% = PEEK WORD(adrs&) CALL MOVETO(x%,y%) WHILE theValue% <> 13 CALL DRAWTEXT(adrs&,0,2) x% = x% + FN TEXTWIDTH(adrs&,0,2): ' "表示幅を設定する" adrs& = adrs& + 2 theValue% = PEEK WORD(adrs&) LONG IF x% >= x2% x% = x1% LONG IF type% = _true y% = y% + fh%: ' "現在のフォントの高さを加える" IF y% > y2% THEN theValue% = 0: ' "下の表示位置を超えたら終了" XELSE theValue% = 0: ' "改行しない場合ははみだしたら即終了" END IF CALL MOVETO(x%,y%) END IF WEND blnk$ = STRING$(255," ") CALL DRAWSTRING(blnk$) CALL DRAWSTRING(blnk$) CALL DRAWSTRING(blnk$) POKE WORD xAdrs&,x1% POKE WORD yAdrs&,y% + fh% err% = FN HUNLOCK(tableH&) END FN
■xFile.GLBL
GLOBALS "xFile.GLBL" END GLOBALS INCLUDE "xFile.INCL" '----------------------------------------------------------- ' "簡易メインルーチン" '----------------------------------------------------------- LOCAL FN loadFile openfile$ = FILES$(_fOpen,"TEXT",,openRefNum%):' "ファイル選択ダイアログの表示" LONG IF openfile$<>"" textH& = FN to2byte(openfile$,1,openRefNum%):' "ファイル内容をメモリに読み込み展開する" CLS ln& = FN countLine(textH&): ' "行数を求める" tableH& = FN getAllLineAddress(textH&): ' "先頭行のアドレスを求める" fh% = USR FONTHEIGHT: ' "フォントの高さを求める" x% = 0 y% = USR FONTHEIGHT dispX% = x% dispY% = y% DY% = -fh% start% = 1 WHILE FN BUTTON = _false FOR i% = start% TO ln& FN PRINT2(tableH&,_true,i%,@x%,@y%,0,0,260,420) NEXT dispY% = dispY% + DY% k$ = INKEY$ IF k$ = " " THEN DY% = DY% * -1 x% = 0 y% = dispY%: ' "1ピクセル上に移動させ表示" IF y% < 0 THEN start% = ABS(DY%) WEND END IF END FN WINDOW #1,"Sample",(0,0)-(320,400),_doc TEXT _applFont,9,,_srcCopy FN loadFile