RTFの保存・読み込みをしてみる ACT-1

今回は、リッチエディットコントロールの内容をリッチテキスト形式(rich text format : RTF)のファイルで保存してみましょう。ただし、この操作にはコールバック関数が必要となるので、通常の方法では実現することができません。そこで、このコールバック関数のマシン語コードを変数に格納して、その変数のアドレスをコールバック関数のアドレスとして指定することにします。

また、サイズが64Kバイト以上のテキストファイルを扱う場合も今回の方法を用いる必要があります。今回は、これについても扱うことにしましょう。

リッチエディットコントロールのストリーム処理

ストリームとは

リッチエディットコントロールへの読み込みや内容の保存は、『ストリーム(stream)』と呼ばれる機能によって行なわれます。これは、アプリケーションとリッチエディットコントロールとの間のデータの入出力を、連続したバイトデータの流れ(stream)であるとみなすという考え方に基づくものです。ちょうど、川の上流から下流に向かって水が流れていくように、リッチエディットコントロールからアプリケーションに向かって、あるいはアプリケーションからリッチエディットコントロールに向かってデータが流れていくような感じです。

リッチエディットからアプリケーションに向かう川(ストリーム)に水(データ)を流し込むと、それはリッチエディットコントロールがデータをストリームに出力したということになります。下流にあたるアプリケーション側ではこの水(データ)を受け取ることができます。

逆に、アプリケーション側を上流として、ここからリッチエディットに向かう川(ストリーム)に水(データ)を流し込むと、下流に位置するリッチエディットではこの流れから水(データ)を取り出します。すなわち、リッチエディットコントロールはデータをストリームから入力したということになります。

川の水の場合は、上流から下流に向かって途切れることなく連続的に流れていきますが、リッチエディットコントロールとやり取りするデータはデジタルデータなのですから、水とまったく同じように連続的に流れていくというわけにはいきません。そこで、リッチエディットコントロールでは、データ全体を小分けにして、小さなデータのかたまりを先頭から順に送る、という方法が取られます。受け取る側では、送り側で送られた順にそれらのデータを受け取ることになります。

ストリーム出力とファイル保存

リッチエディットコントロールからアプリケーションにデータを送る必要がある場合、まずはリッチエディットコントロールがデータをストリームに出力し、アプリケーション側がそれをストリームから受け取る必要があります。それにはまず、アプリケーションがリッチエディットコントロールに「データをストリームに出力してくれ」という指令(EM_STREAMOUT メッセージ)を送ります。この指令を受け取ったリッチエディットコントロールは、データを小分けにし、先頭から順番にデータを送り出して行くのです。

さて、アプリケーション側では、このストリームからデータを受け取らなければなりません。リッチエディットコントロールの場合、ストリームによるのデータ転送が行なわれるときに、アプリケーション側が準備したコールバック関数が呼び出されることになっています。このコールバック関数は、小分けにされたデータのかたまり1つ1つが送られるたびに呼び出されます。このコールバック関数には、小分けにされた1つのデータのかたまりとそのサイズなどの情報が引数として渡されてきます。コールバック関数側では、このデータを処理して行きます。小分けにされたデータは先頭から順番に送られてくるので、コールバック関数内でシーケンシャルファイル処理によって保存していけば、すべてのデータを1つのファイルに保存することができるということが分かりますね。

リッチエディットコントロールにデータ転送の指令(EM_STREAMOUT メッセージ)を送るとき、アプリケーションは任意の32ビット値をセットすることができます。この32ビット値は、コールバック関数が呼ばれるときにその引数の1つとして渡されます。一般的には、ファイルを書き込みアクセスでオープンしておき、そのファイルハンドル(あるいはメンバとしてハンドルを含む構造体のアドレス)が32ビット値として指定されます。そして、コールバック関数内でこのファイルハンドルが指すファイルに書き込んでいくようにすれば、コントロールの内容をファイルに保存することができるのです。

ストリーム入力とファイル読み込み

アプリケーションからリッチエディットコントロールにデータを送る必要がある場合、アプリケーションがデータをストリームに出力して、リッチエディットコントロールがそれをストリームから入力する必要があります。それには、まず、アプリケーションはリッチエディットコントロールに「これから○○形式のデータを送りたいから、データを受け取る準備をしてくれ」という指令(EM_STREAMIN メッセージ)を送ります。すると、リッチエディットコントロールは、アプリケーションに「○○バイト分だけデータを受け取りたいから、その分だけのデータを出力してくれ」という指令を出します。この指令は、アプリケーション側が準備したコールバック関数を呼び出すという形で行なわれます。アプリケーション側は、コールバック関数の中で、送りたいデータの先頭から指定されたバイトぶんだけ順番に送らなくてはなりません。この指令(コールバック関数の呼び出し)は、すべてのデータが送られるまで繰り返し行なわれるので、アプリケーションはそのたびにデータを送り出していくのです。ファイルからデータを読み込んでそれを送る場合は、シーケンシャル処理によって読み込んでいけば、先頭から順番にデータを取り出してリッチエディットコントロールに送り出していくことができますね。

EM_STREAMIN メッセージを送る場合もアプリケーションは任意の32ビット値をセットすることができて、この値がコールバック関数の引数として渡されてきます。したがって、ファイルを読み取り属性でオープンしておいて、そのファイルハンドル(あるいはメンバとしてハンドルを含む構造体のアドレス)をこの値として指定しておき、コールバック関数内でこのハンドルが指すファイルを先頭から順に読み込んでいくようにすれば、ファイルのデータをリッチエディットコントロールに読み込ませることができます。

ファイルへの保存

まずはリッチエディットコントロールの内容をファイルに保存する方法からです。その手順は以下のようになります。

  1. コールバック関数のコードを変数に格納する。
  2. CreateFile 関数を呼び出して、書き込みアクセス指定でファイルをオープンする。
  3. ファイルハンドルと WriteFile 関数のアドレスを構造体(事実上は配列変数)に格納する。
  4. EDITSTREAM 構造体に、コールバック関数コードを入れた変数のアドレスと、ファイルハンドルおよび WriteFile 関数のアドレスを入れた構造体のアドレスを格納し、リッチエディットコントロールに EM_STREAMOUT メッセージを送信する。
  5. CloseHandle 関数でファイルハンドルをクローズする。

まず、コールバック関数のマシン語コードを変数に格納します。これは、リッチエディットのストリーム入出力で使用される特定の形のコールバック関数( EditStreamCallback コールバック関数)の形で定義されたものでなければなりません。今回は、ファイル保存用のコールバック関数のコード(RESaveProc サンプルコード)を筆者が準備したので、これを使用することにします。

ここでひとつ注意しておくことは、マシン語を格納する変数を、必ず xdim 命令を使って確保しなければいけないということです。この命令は、次のファイルをダウンロードしてインクルードすることで使用できるようになります。

ダウンロード (xdim.as)

#include "xdim.as"

xdim fnRESaveProc, 9
fnRESaveProc.0 = $1024448b, $0c244c8b, $0824548b, $8b50006a, $510c2444
fnRESaveProc.5 = $51088b52, $f70450ff, $40c01bd8, $000010c2

次に、 CreateFile 関数を呼び出して、保存するファイルをオープン(新しく作成)しておきます。このときにファイルハンドルが返されますが、このファイルハンドルは情報としてコールバック関数に渡されることになります。

リッチエディットコントロールからのストリーム出力処理を行なうためには、 EDITSTREAM 構造体に必要な情報を格納してから EM_STREAMOUT メッセージを送信します。

EDITSTREAM 構造体は以下のように定義されています。

typedef struct _editstream {
    DWORD_PTR  dwCookie;              // アプリケーション定義値
    DWORD      dwError;               // エラーコード
    EDITSTREAMCALLBACK pfnCallback;   // コールバック関数アドレス
} EDITSTREAM;

dwCookie メンバは、コールバック関数に渡される任意の32ビット値です。このメンバに指定された値が、必要な情報としてそのままコールバック関数に渡されることになります。

dwError には、メッセージ送信後にエラーコードが格納されます。

pfnCallback メンバには、コールバック関数のアドレスを指定します。このメンバには、コールバック関数のマシン語コードを入れた変数のアドレスを指定しておきます。

コールバック関数に情報を渡したい場合には、その情報を dwCookie メンバに格納することができるわけですが、今回コールバック関数に渡したい情報には、まず、保存するためのファイルハンドルがありますね。しかし、渡さなければならない情報は実はこれだけではありません。

というのも、今回使用されるコールバック関数のマシン語コードには、コールバック関数の中で使用されているAPI関数アドレスの情報が含まれていないのです。なぜなら、API関数アドレスは、その関数を提供するDLLがメモリ上にロードされて初めて決定されるため、実行されるまではそのアドレスはわからないからです。したがって、API関数のアドレスの情報を、ファイルハンドルといっしょにコールバック関数に渡してやる必要があるのです。今回使用する RESaveProc サンプルコードでは WriteFile 関数を呼び出しているので、この関数のアドレスを渡す必要があります。

今回のように、コールバック関数に2つ以上の情報を渡すためには、それらの情報をすべて格納できるだけの構造体を定義して、その構造体に情報を格納し、構造体のアドレス1つだけをコールバック関数に渡す、という方法をとります。今回使用する RESaveProc サンプルコードでは、ファイルハンドルと WriteFile 関数のアドレスの2つの情報を格納するために、以下のような構造体を新たに定義することにしました。

typedef struct {
    HANDLE hFile;          // ファイルハンドル
    FARPROC pfnWriteFile;  // WriteFile 関数のアドレス
} RESAVEDATA;

この構造体の hFile メンバには GENERIC_WRITE アクセス(書き込みアクセス)を持つファイルハンドルを、また、 pfnCallback メンバには WriteFile 関数のアドレスをそれぞれ格納します。そして、この構造体のアドレスを EDITSTREAM 構造体の dwCookie メンバに格納します。

WriteFile 関数のアドレスの取得には通常、 LOADLIB.DLL の ll_getproc 命令を使用しますが、今回は代わりに dll_getfunc 命令を使用します。この命令はLLMODモジュールで定義されている命令で、第3パラメータにはDLLハンドルのほかに、 D_KERNEL や D_GDI などの値を指定することができます。

EDITSTREAM 構造体に必要な情報を格納できたら、 EM_STREAMOUT メッセージを送信してストリーム出力処理を行ないます。

#define  EM_STREAMOUT      0x044A

EM_STREAMOUT
    wParam = uFormat;
    lParam = pStream;

uFormat パラメータには、リッチエディットコントロールからストリームに出力するデータの形式を指定します。 1 (SF_TEXT) を指定するとテキスト形式に、 2 (SF_RTF) を指定するとリッチテキスト形式(RTF)になります。ここで指定した形式でファイルに保存されることになります。

pStream パラメータには、情報を格納した EDITSTREAM 構造体のアドレスを指定します。

このメッセージ送信によって、コールバック関数が呼び出され、送られたデータ全体がファイルに保存されることになります。

最後に、 CloseHandle 関数でファイルハンドルをクローズします。

ファイルからの読み込み

次に、ファイルを開いてリッチエディットコントロールに読み込む方法です。その手順は以下のようになります。

  1. コールバック関数のコードを変数に格納する。
  2. CreateFile 関数を呼び出して、読み取りアクセス指定でファイルをオープンする。
  3. ファイルハンドルと ReadFile 関数のアドレスを構造体(事実上は配列変数)に格納する。
  4. EDITSTREAM構造体に、コールバック関数コードを入れた変数のアドレスと、ファイルハンドルおよび ReadFile 関数のアドレスを入れた構造体のアドレスを格納し、リッチエディットコントロールに EM_STREAMIN メッセージを送信する。
  5. CloseHandle 関数でファイルハンドルをクローズする。

手順は保存のときとほとんど同じです。

コールバック関数のマシン語コードを変数に格納するのですが、コールバック関数の中で行なう処理は保存のときとは異なるので、別のコールバック関数を準備する必要があります。コールバック関数は、先ほどと同じく EditStreamCallback コールバックの形式で定義されたものでなければなりません。今回は、ファイルロード用のコールバック関数のコード(RELoadProc サンプルコード)を筆者が準備したので、これを使用します。

xdim fnRELoadProc, 9
fnRELoadProc.0 = $1024448b, $0c244c8b, $0824548b, $8b50006a, $510c2444
fnRELoadProc.5 = $51088b52, $f70450ff, $40c01bd8, $000010c2

次に、読み込むファイルを CreateFile 関数を呼び出してオープンし、ファイルハンドルを取得します。このファイルハンドルは情報としてコールバック関数に渡されることになります。

リッチエディットコントロールへのストリーム入力処理を行なうためには、ストリーム出力のときと同じく、まず EDITSTREAM 構造体に必要な情報を格納します。そして、その後に EM_STREAMIN メッセージを送信します。

保存のときと同じく、ここでも、ファイルハンドルとAPI関数アドレスの情報をコールバック関数に渡す必要があります。コールバック関数ではファイル読み込みのために ReadFile 関数が使用されているので、この関数アドレスを渡す必要があります。そこで、今回使用する RELoadProc サンプルコードでは、この2つの情報を格納するために以下のような構造体を新たに定義して使用しています。

typedef struct {
    HANDLE hFile;          // ファイルハンドル
    FARPROC pfnReadFile;   // ReadFile 関数のアドレス
} RELOADDATA;

この構造体の hFile メンバには GENERIC_READ アクセス(読み取りアクセス)を持つファイルハンドルを、また、 pfnCallback メンバには ReadFile 関数のアドレスをそれぞれ格納します。そして、この構造体のアドレスを EDITSTREAM 構造体の dwCookie メンバに格納します。

EDITSTREAM 構造体に必要な情報を格納できたら、 EM_STREAMIN メッセージを送信して、ストリーム入力処理を行ないます。

#define  EM_STREAMIN       0x0449

EM_STREAMIN
    wParam = uFormat;
    lParam = pStream;

uFormat パラメータには、ストリームから入力するデータの形式(リッチエディットコントロールに送るデータの形式)を指定します。ファイルのデータの形式がテキスト形式の場合は 1 (SF_TEXT) を、リッチテキスト形式(RTF)の場合は 2 (SF_RTF) を指定します。

pStream パラメータには、情報を格納した EDITSTREAM 構造体のアドレスを指定します。

このメッセージ送信によって、コールバック関数が呼び出され、ファイルからデータ全体がリッチエディットコントロールに送られることになります。

最後に、 CloseHandle 関数でファイルハンドルをクローズします。

コールバック関数の処理

さて、 EM_STREAMOUT メッセージや EM_STREAMIN メッセージで呼び出されるコールバック関数の処理について触れておきましょう。

EM_STREAMOUTEM_STREAMIN メッセージ送信時に呼び出されるコールバック関数は、以下のような形式(EditStreamCallback の形)をしたものでなくてはなりません。

DWORD CALLBACK EditStreamCallback(
    DWORD   dwCookie,  // 任意の32ビット値
    LPBYTE  pbBuffer,  // データバッファのアドレス 
    LONG    cb,        // サイズ
    LONG   *pcb        // 実際に処理したサイズ
);

ここで、関数はすべて、そのコードがあるメモリ上のアドレスで指定されるために EditStreamCallback という関数名は意味を持ちません。便利のためにそのように名づけられているだけです。

この関数の第1引数(dwCookie パラメータ)には、メッセージ送信時に EDITSTREAM 構造体の dwCookie メンバに指定された値が渡されることになります。今回、このパラメータには、ファイルハンドルを含む構造体のアドレスを渡すことになります。

では、他の引数を見てみましょう。これらの引数の意味が WriteFile 関数や ReadFile 関数の引数とほとんど同じであることに気付くでしょうか。実際、今回使用しているコールバック関数( RESaveProcRELoadProc )では、これらの引数をそのまま WriteFile 関数や ReadFile 関数に渡すという処理をしているだけなのです。


今回は解説が長くなってしまったので、スクリプトは次回に持ち越しです。