不定形リージョン作成関数

前回までは Windows システムから呼び出されるコールバック関数を作成していましたが、今回はスクリプト中から直接呼び出す関数を作成してみましょう。といっても、作成方法は前回までと変わりません。

今回作成する関数は、 HSP ウィンドウに描画されているビットマップと同じ形のリージョンを作成し、これをウィンドウに設定することで、ウィンドウの形をビットマップと同じ形にしてしまうというものです。これは loadlib.dll のみで実現させることができますが、高速化させることを考えるなら、マシン語コードで実現したいところです。


リージョンについての詳しい説明はなしにして、さっそくソースコードを書いていきましょう。今回のように関数を直接呼び出す場合には、マシン語コードの中に情報を埋め込む必要はありません。必要な情報はすべて関数の引数として渡せばいいのです。

今回はビットマップデータに直接アクセスして、指定された色(透過色)以外の部分にリージョンが設定されるようにしていきます。また、ウィンドウ初期化モードがパレットモードの場合でもフルカラーモードの場合でも実行させることができるように、両方のモードに対応させましょう。そこで、関数内部で必要となる情報は、

といったところでしょう。これらのうち、最初の4つはすべて BMSCR 構造体に含まれる情報であるので、ウィンドウの BMSCR 構造体のアドレスを引数に渡すことにします。また、3つの API 関数のアドレスは以下のように定義された構造体に格納して、そのアドレスを渡すことにしましょう。(別々の引数として渡してもいいのですが。)

typedef struct {
    HRGN (WINAPI *pfnCreateRectRgn) (int,int,int,int);
    int  (WINAPI *pfnCombineRgn) (HRGN,HRGN,HRGN,int);
    BOOL (WINAPI *pfnDeleteObject) (HGDIOBJ);
} RGN_FUNC;

それぞれのメンバ名には、対応する関数名に“pfn”をつけてあります。(関数名と同じ名前を使うと、問題が発生する場合があるので。) また、コンパイラの型チェックを使用するために、今回は FARPROC 型ではなく、関数のプロトタイプ(関数型と引数の型の宣言)も同時に記述してあります。

そして、今回定義する関数は以下のようになります。関数呼び出し時の形式を合わせるために、プレフィックス WINAPI を付けましょう。 WINAPI は、ヘッダファイルの中で、呼び出し形式を表すプレフィックス __stdcall につけられている別名です。ですから、 WINAPI の代わりに __stdcall を指定してもよいです。

HRGN WINAPI CreateRgnFromBmp(
    BMSCR *pBmscr,         // ウィンドウのBMSCR構造体アドレス
    DWORD  crTransparent,  // 透過色(COLORREFまたはパレットインデックス)
    RGN_FUNC *pFunc        // RGN_FUNC構造体アドレス
);

pBmscr パラメータには BMSCR 構造体のアドレスが渡されるようにします。 crTransparent パラメータは、ウィンドウがフルカラーモードの場合は、透過色を RGB 値で指定した値です。パレットモードの場合は透過色のパレットインデックスです。 pFunc は、 HSP スクリプト側で取得した API 関数のアドレスを格納した RGN_FUNC 構造体のアドレスです。この関数は作成したリージョンのハンドルを返します。


実際のソースコードは以下のようになります。

ソースファイル bmprgn.c

#include <windows.h>
#include "hspdll.h"

// 関数アドレスを格納する構造体
typedef struct {
    HRGN (WINAPI *pfnCreateRectRgn) (int,int,int,int);
    int  (WINAPI *pfnCombineRgn) (HRGN,HRGN,HRGN,int);
    BOOL (WINAPI *pfnDeleteObject) (HGDIOBJ);
} RGN_FUNC;

HRGN WINAPI CreateRgnFromBmp(
    BMSCR *pBmscr,              // ウィンドウのBMSCR構造体アドレス
    DWORD crTransparent,        // 透過色(COLORREFまたはパレットインデックス)
    RGN_FUNC *pFunc)            // RGN_FUNC構造体アドレス
{
    HRGN hRgn, hRgnTemp;        // リージョンハンドル
    BYTE *pBit;                 // 現在のピクセルへのポインタ
    DWORD crPixel;              // 現在のピクセルの色
    int x, y;                   // 現在のピクセルの座標
    int xx;                     // 透過色開始位置のx座標を保存
    int nPixelSize;             // 1ピクセルのバイト数

    BYTE *pBitmap = pBmscr->pBit;       // ビットマップデータへのポインタ
    BOOL bPalette = pBmscr->palmode;    // パレットモードかどうか
    int nWidth = pBmscr->sx;            // x サイズ
    int nHeight = pBmscr->sy;           // y サイズ

    // 各関数のアドレス
    HRGN (WINAPI *pfnCreateRectRgn) (int,int,int,int) = pFunc->pfnCreateRectRgn;
    int  (WINAPI *pfnCombineRgn) (HRGN,HRGN,HRGN,int) = pFunc->pfnCombineRgn;
    BOOL (WINAPI *pfnDeleteObject) (HGDIOBJ)          = pFunc->pfnDeleteObject;

    // 1ピクセルのデータサイズ
    if (bPalette)
        nPixelSize = 1;
    else
        nPixelSize = 3;

    // 空のリージョン作成
    hRgn = pfnCreateRectRgn(0,0,0,0);

    // 縦横にビットが透過色かを調べ、透過色でない部分をリージョンに追加
    for (y = 0; y < nHeight; y++) {     // 縦方向
        // ポインタをその行の先頭のピクセルに合わせる
        pBit = pBitmap + (( nHeight - 1 - y ) * nWidth * nPixelSize);

        for (x = 0; x < nWidth; x++) {  // 横方向
            // 現在のピクセルの色を取得
            if (bPalette)
                crPixel = pBit[0];
            else
                crPixel = RGB(pBit[2], pBit[1], pBit[0]);

            // 透過色かどうかを調べる
            if (crPixel != crTransparent) {
                // 透過色ではない場合
                xx = x;     // x を保存(透過色ではない点の始まりを保存)
                            // 連続した透過色ではない領域を求める
                for (x = x + 1; x < nWidth; x++) {
                    // ポインタを次のピクセルへ
                    pBit += nPixelSize;

                    // 現在のピクセルの色を取得
                    if (bPalette)
                        crPixel = pBit[0];
                    else
                        crPixel = RGB(pBit[2], pBit[1], pBit[0]);

                    // 透過色になったら break
                    if (crPixel == crTransparent)
                        break;
                }

                // 一時的な透過色でない領域のリージョンを作成
                hRgnTemp = pfnCreateRectRgn(xx ,y ,x ,y+1);
                // hRgn と合成(ビットマップ全体のリージョンを作成)
                pfnCombineRgn(hRgn, hRgn, hRgnTemp, RGN_OR);
                // 一時的なリージョンを削除
                pfnDeleteObject(hRgnTemp);
            }
            // ポインタを次のピクセルへ
            pBit += nPixelSize;
        }
    }
    return hRgn;
}

これまでのものと比べると、ソースコードはかなり長くなっていますね。出力されるマシン語コードの長さも半端じゃなくなりそうな予感です。前回までと同じように、コンパイルおよびマシン語情報の抽出を行ないます。

C:\WINDOWS>d:

D:\>cd hspmcn

D:\>mcnbcc bmprgn

D:\hspmcn>bcc32 -c -O2 bmprgn.c
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
bmprgn.c:

D:\hspmcn>tdump -oxCOMENT bmprgn.obj bmprgn.txt
Turbo Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation

D:\hspmcn>
(…………………… 省   略 ……………………)
000DFE PUBDEF  'CreateRgnFromBmp'      Segment: _TEXT:0000
000E34 LEDATA  Segment: _TEXT          Offset: 0000  Length: 013B
    0000: 55 8B EC 83 C4 D4 53 56  57 8B 55 10 8B 45 08 8B   U駆ζヤSVW偽.畿.稀
    0010: 48 14 89 4D EC 8B 48 0C  89 4D E8 8B 48 04 89 4D    .窺・H.窺闍H.窺
    0020: E4 8B 40 08 89 45 E0 8B  02 89 45 DC 8B 4A 04 89   芫@.右煖.右ワ徽.窺
    0030: 4D D8 8B 52 08 89 55 D4  83 7D E8 00 74 07 BF 01    リ騎.蔚ヤマ..t.ソ.
    0040: 00 00 00 EB 05 BF 03 00  00 00 6A 00 6A 00 6A 00   .....ソ....j.j.j.
    0050: 6A 00 FF 55 DC 89 45 FC  33 C0 89 45 F4 8B 55 F4   j..Uワ右.3タ右偽.
    0060: 3B 55 E0 0F 8D C6 00 00  00 8B 5D E0 33 F6 4B 2B   ;U..哉...犠.3K+
    0070: 5D F4 0F AF 5D E4 0F AF  DF 03 5D EC 3B 75 E4 0F   ]..ッ]..ッ゚.].;u..
    0080: 8D 9B 00 00 00 83 7D E8  00 74 06 33 C0 8A 03 EB   惚...マ..t.3タ...
    0090: 1B 33 D2 33 C0 8A 53 01  8A 43 02 0F B7 CA 33 D2   .3メ3タ慨.海..キハ3メ
    00A0: C1 E1 08 8A 13 C1 E2 10  0B C1 0B C2 3B 45 0C 74   チ....チ...チ.ツ;E.t
    00B0: 63 89 75 F0 46 3B 75 E4  7D 34 03 DF 83 7D E8 00   c疫F;u艟4.゚マ..
    00C0: 74 06 33 C0 8A 03 EB 1B  33 D2 33 C0 8A 53 01 8A   t.3タ....3メ3タ慨.海
    00D0: 43 02 0F B7 CA 33 D2 C1  E1 08 8A 13 C1 E2 10 0B    ..キハ3メチ....チ...
    00E0: C1 0B C2 3B 45 0C 74 06  46 3B 75 E4 7C CC 8B 45   チ.ツ;E.t.F;u艚フ畿
    00F0: F4 40 50 56 FF 75 F4 FF  75 F0 FF 55 DC 89 45 F8   @PV.u..u..Uワ右j
    0100: 6A 02 FF 75 F8 FF 75 FC  FF 75 FC FF 55 D8 FF 75    ..u..u..u..Uリ.u
    0110: F8 FF 55 D4 03 DF 46 3B  75 E4 0F 8C 65 FF FF FF   ..Uヤ.゚F;u..憩...
    0120: FF 45 F4 8B 4D F4 3B 4D  E0 0F 8C 3A FF FF FF 8B   .E貴.;M...:...畿
    0130: 45 FC 5F 5E 5B 8B E5 5D  C2 0C 00                   ・^[句]ツ..
000F76 MODE32

一言で言えば、長いです。長すぎます。こんなの、一字一字抜き出して変換してられるかってほどに。

根性がある人は、コードを抜き出して変換してみましょう。他にも、矩形の選択範囲を指定できるテキストエディタ(秀丸エディタのBOX範囲選択機能など)ならば、とっても簡単にコード部分だけを抜き出して、前回までのバイナリ変換プログラムに掛けることができてしまいます。最終的にはこうなりますが。

xdim fncode, 79
fncode.0 = $83ec8b55, $5653d4c4, $10558b57, $8b08458b, $4d891448, $0c488bec
fncode.6 = $8be84d89, $4d890448, $08408be4, $8be04589, $dc458902, $89044a8b
fncode.12 = $528bd84d, $d4558908, $00e87d83, $01bf0774, $eb000000, $0003bf05
fncode.18 = $006a0000, $006a006a, $55ff006a, $fc4589dc, $4589c033, $f4558bf4
fncode.24 = $0fe0553b, $0000c68d, $e05d8b00, $2b4bf633, $af0ff45d, $af0fe45d
fncode.30 = $ec5d03df, $0fe4753b, $00009b8d, $e87d8300, $33067400, $eb038ac0
fncode.36 = $33d2331b, $01538ac0, $0f02438a, $d233cab7, $8a08e1c1, $10e2c113
fncode.42 = $c20bc10b, $740c453b, $f0758963, $e4753b46, $df03347d, $00e87d83
fncode.48 = $c0330674, $1beb038a, $c033d233, $8a01538a, $b70f0243, $c1d233ca
fncode.54 = $138a08e1, $0b10e2c1, $3bc20bc1, $06740c45, $e4753b46, $458bcc7c
fncode.60 = $565040f4, $fff475ff, $55fff075, $f84589dc, $75ff026a, $fc75fff8
fncode.66 = $fffc75ff, $75ffd855, $d455fff8, $3b46df03, $8c0fe475, $ffffff65
fncode.72 = $8bf445ff, $4d3bf44d, $3a8c0fe0, $8bffffff, $5e5ffc45, $5de58b5b
fncode.78 = $00000cc2

ただ、これだとスクリプトが少々長くなってしまいますし、メモ帳などのように矩形選択機能のないエディタを使ってる人にはどうしようもありませんね。そこで、今回はちょっとズルをすることにします。

ソースコードをコンパイルすると、オブジェクトファイル(拡張子 .obj )が出力されます。このオブジェクトファイルには、上のマシン語コードがバイナリ形式で含まれているので、それをそのまま使用することにします。ただし、このオブジェクトファイル全体が作成した関数のコードというわけではなく、実際の関数コードはこのファイルの一部に過ぎません。したがって、このファイルの中から関数コード部分を見つけ出して、その部分を抜き出してくる必要があります。

まずは、オブジェクトファイルをバイナリエディタで開きましょう。基本的にはどのバイナリエディタでも問題ないと思います。ちなみに、筆者は Stirling を使用しています。

本来なら、オブジェクトファイルのヘッダ部分から順にたどっていかなくてはならないのですが、なにぶん面倒ですよね。そこで、上にリスティングファイルを見てみると、関数コードの先頭部分が 55 8B EC 83 C4 D4 53 56 …… となっていることが分かりますね。そして、『Length: 013B』の表示から分かるように、このコード全体のサイズが $13B (315) バイトであることも分かります。

そこで、オブジェクトファイルをバイナリエディタで開いたら、いきなり 55 8B EC 83 C4 D4 53 56 を検索してしまいましょう。(検索時のデータ種別は文字列ではなく16進データ(バイナリ形式)で。) すぐに見つかると思います。2つ以上見つかることはまずないと思いますが、もしも見つかった場合は、その後に続くコードから、どちらが該当するコードなのかを判断しましょう。

コードの先頭が見つかったら、そこから $13B (315) バイトまでの領域を選択します。たいていのバイナリエディタでは、選択領域のサイズがステータスバーに表示されると思いますので、それを見ながら行ないましょう。(10進数と16進数を間違えないように。) 少し余裕を持たせて多めに選択してしまってもかまいません。

後は、その部分をコピーしてから、新しいファイルとして貼り付けて保存すれば OK です。 Stirling を使用している場合は、選択領域を右クリックするとメニューが表示されるので、その中の「選択領域をファイルに保存」でも行なうことができます。ファイル名は何でもいいですが、ここでは rgn.mcn という名前を付けて保存することにします。

これで、マシン語の関数コードを含むファイルが作成されました。あとは、 HSP 側でこのファイルをいったん変数に呼び込んでから、この関数コードを呼び出せばいいのです。


実際にサンプルを書いてみましょう。ビットマップファイル test.bmp を各自で準備してください。このビットマップの、白で表示される部分を透明化して表示します。

    #include "llmod.as"
    #include "xdim.as"

    screen 0, 300, 200

    ; 画像のロード
    bgscr 2,0,0,0
    picload "test.bmp"

    ; 関数コードの読み込み  
    xdim fncode, 79
    bload "rgn.mcn", fncode, 315

    ; 関数アドレス取得
    getptr pfunc, fncode

    ; RGN_FUNC 構造体にAPI関数アドレスを格納
    dll_getfunc rgnfunc.0, "CreateRectRgn", D_GDI
    dll_getfunc rgnfunc.1, "CombineRgn",    D_GDI
    dll_getfunc rgnfunc.2, "DeleteObject",  D_GDI

    ; 指定された色を透過色としてリージョン作成
    mref bmscr, 67              ; BMSCR構造体を割り当てる
    getptr pm.0, bmscr          ; BMSCR構造体のアドレス
    pm.1 = $FFFFFF              ; 透過色を白に指定(RGB値 $FFFFFF)
    getptr pm.2, rgnfunc        ; RGN_FUNC構造体のアドレス
    ll_callfunc pm, 3, pfunc    ; 関数コード呼び出し
    hRgn = dllret               ; 作成されたリージョンハンドル

    ; リージョンをウィンドウに割り当てる
    pm = bmscr.13, hRgn, 1
    dllproc "SetWindowRgn", pm, 3, D_USER
    stop