このプログラムでは、メインウインドウの他に「お尻」を表示してマウスダウンなどを受け入れるサブウインドウ、ボタン、回数表示のラベルを配置しました。特に、お尻のウインドウは独自のウインドウプロシージャを持ち、お尻の部分に対する入力や描画を独自に処理しています。
また、データとしてはお尻と手のビットマップ、サウンドファイル、アイコン、メニューがあります。このうちお尻のビットマップとサウンドファイルはファイルから読み込みますが、他はリソースとしました。ただし、手のビットマップは、120×72ピクセルの24ビットDIBのピクセル列を書き出したカスタムのバイナリリソースです。
ビットマップを読み込んだらお尻ウインドウにビットマップを表示し、後はひたすらそのお尻をたたいて行くわけですが、お尻をたたいて赤くなっていく様子を描く必要があります(っていうか、それだけのソフトですね)。通常のグラフィックソフトでは24ビットフルカラーのDIBを出力しますが、今回は扱いやすいように1ピクセル32ビットの32ビットDIBに変換する事にしました。
処理としては、まず24ビットのビットマップを読み込み次に32ビットのビットマップ用バッファを用意します。今回は480×320ピクセルのビットマップを使うので、ビットマップを保持する32ビットDIBは、sizeof(BITMAPINFO)+480*320*4バイトになります。
まず、ビットマップの読み込みは
fh=CreateFile(fn,GENERIC_READ,0,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL); /* ファイルオープン */
lpWork=GlobalAlloc(GPTR,GetFileSize(fh,NULL)); /* バッファ確保 */
ReadFile(fh,lpWork,GetFileSize(fh,NULL),&dummy,NULL);
lpInfo=(LPBITMAPINFO)((LPBYTE)lpWork+sizeof(BITMAPFILEHEADER));
CloseHandle(fh);
という感じですね。これで、ビットマップファイル(ファイル名fn)をバッファlpWorkに読み込みました。この場合、BITMAPINFOの先頭はlpWork+sizeof(BITMAPFILEHEADER)、ビットマップのピクセル列はlpWork+(*(LPDWORD)((LPBYTE)lpWork+10))からになります。
では、次に実際に処理を行うビットマップバッファの確保。今回は、「最初の状態」を保存しておきたいのでビットマップのピクセル用に480×320×4×2バイト確保します。バッファは、GPTRでゼロクリアしておきましょう。
lpDIB=GlobalAlloc(GPTR,sizeof(BITMAPINFO)+480*320*4*2);
バッファを確保したら、lpInfoのビットカウントを32にしてからバッファにコピーします。また、ピクセル列の先頭ポインタも設定しておきましょう。
lpInfo->bmiHeader.biBitCount=32; CopyMemory(lpDIB,lpInfo,sizeof(BITMAPINFO)); dwOffset=*(LPDWORD)((LPBYTE)lpWork+10); /* ピクセル列の先頭ポインタ */ lpBMP=(LPDWORD)((LPBYTE)lpDIB+sizeof(BITMAPINFO));
これで、ピクセル(x, y)をDWORD型配列lpBMP[x+y*480]という形で扱えるようになりました。
次はピクセル列のコピー。ピクセル列の先頭は、読み込んだビットマップがlpWork+dwOffset、処理用のビットマップがlpBMPでした。32ビットDIBは下位3バイトにRGBが入るので、読み込んだビットマップのピクセルから3バイト持ってきてビットマップの対応ピクセルの先頭3バイト(アドレス上は上位と下位が逆転している)に書き込む処理を行います。ここでは、読み込んだビットマップは1ピクセル3バイトで処理用のポインタlpBMPは32ビット整数型ポインタ(LPDWORD)である点に注意してください。
for (i=0;i<320;i++)
for (j=0;j<480;j++)
CopyMemory(lpBMP+i*480+j,(LPBYTE)lpWork+dwOffset+i*480*3+j*3,3);
最後に、以下のようにしてlpOriに最初の状態を保存しておきます。
CopyMemory(lpOri,lpBMP,480*320*4);
ビットマップは、最初だけでなくドラッグ&ドロップでも読み込むので、エラーチェックを入れてみました。ドロップされた時には、ドロップされたファイルを読み込んで読み込みに成功したら状態を初期化します。
case WM_DROPFILES: /* ファイルがドロップされた時の処理 */
hDrop=(HDROP)wParam; /* HDROPを取得 */
DragQueryFile(hDrop,0,lpszFn,256); /* ファイル名を取得 */
if (readBMP(lpszFn)) { /* 読み込み成功 */
dwSpank=0;
SetWindowText(hwLabel,"0");
if (fSpanking) { /* 自動進行中 */
KillTimer(hwnd,1); /* タイマーストップ */
EnableWindow(hwReset,TRUE);
SetWindowText(hwAuto,"自動");
fSpanking=FALSE;
}
}
DragFinish(hDrop); /* ドラッグ&ドロップ終了処理 */
break;
ビットマップの処理は読み込みだけでなく書き込みも行いますが、こちらはビットマップを32ビットから24ビットに変換しヘッダを付けてファイルに書き出しています。
・ビットマップの保存処理
/* バッファ確保とポインタ設定 */
lpBuf=GlobalAlloc(GPTR,sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+480*320*3);
lpHead=(LPBITMAPFILEHEADER)lpBuf;
lpInfo=(LPBITMAPINFOHEADER)((LPBYTE)lpBuf+sizeof(BITMAPFILEHEADER));
lpPixel=(LPBYTE)lpBuf+sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER);
/* ビットマップのヘッダ作成 */
lpHead->bfType='M'*256+'B';
lpHead->bfSize=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+480*320*3;
lpHead->bfOffBits=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER);
lpInfo->biSize=sizeof(BITMAPINFOHEADER);
lpInfo->biWidth=480;
lpInfo->biHeight=320;
lpInfo->biPlanes=1;
lpInfo->biBitCount=24;
for (i=0;i<320;i++) /* 現在のビットマップをバッファにコピー */
for (j=0;j<480;j++)
CopyMemory(lpPixel+i*480*3+j*3,lpBMP+i*480+j,3);
/* バッファをファイルに書き出す */
fh=CreateFile(lpszFn,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
WriteFile(fh,lpBuf,sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFO)+480*320*3,&dwSize,NULL);
if (dwSize<sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFO)+480*320*3)
MessageBox(hwnd,"ファイル作成に失敗しました。空き容量を確認してください",
"書き込みエラー",MB_OK);
CloseHandle(fh);
今回は、手のスプライト表示が入るためお尻の他に表示用のビットマップも用意して実際の表示ではこのビットマップを表示するようにしました。手のビットマップも、リソースから読み込んで32ビットのピクセル列を作成しておきます(ただし、手はスプライト処理のパターンとして使うだけなのでヘッダは不要)。
お尻の部分ではカーソルが消えて手が表示されますが、これはマウスカーソルを空白のパターンに設定して手のビットマップを表示する事で実現しています。手のビットマップは、背景を青(0x000000ff)にしてあるので、まず表示用ビットマップにお尻を描いてから手のビットマップを青を抜き色にスプライト表示すれば良いわけです。今回は、手を表示する時の中心点を(50, 36)にしたので、実際に手を描き込む処理は以下のようになります。
/* 表示用ビットマップにお尻を描画 */
CopyMemory(lpBBMP,lpBMP,480*320*4);
if (hx>0) /* 表示用ビットマップに手を描き込む */
for (i=0;i<72;i++)
for (j=0;j<120;j++)
if ((hx-50)+j>=0 && (hx-50)+j<480 && (hy-36)+i>=0 &&
(hy-36)+i<320 && lpHBMP[j+i*120]!=255)
lpBBMP[hx-50+j+(hy+i-36)*480]=lpHBMP[j+i*120];
お尻をたたいた時の処理は、「たたいた部分を赤くする」ですね。もう少し具体的に言えば、「たたかれた部分のR成分を増やしGB成分を減らして赤みを増やす」処理、という事になります。お尻をたたく時は、マウスの左クリックか乱数で決めた座標を指定するので、そこをたたく「中心点」にしましょう。たたいた時には、中心点の周辺を赤くして行けば良いわけです。例えば、たたかれた中心点を中心とする80×80ピクセルの正方形領域を赤くするには、以下のようにします。
for (i=-40;i<40;i++) /* たたかれた部分の赤みを増す */
for (j=-40;j<40;j++) {
/* 処理の対象ピクセルがお尻の部分に入っているか? */
if (x+j>=0 && x+j<480 && y+i>=0 && y+i<320 &&
lpBMP[x+j+(y+i)*480]!=255) {
/* ピクセルの現在の成分取得 */
r=(lpBMP[x+j+(y+i)*480] & 0x00ff0000) >> 16;
g=(lpBMP[x+j+(y+i)*480] & 0x0000ff00) >> 8;
b=lpBMP[x+j+(y+i)*480] & 0x000000ff;
/* ピクセルの初期状態の成分取得 */
or=(lpOri[x+j+(y+i)*480] & 0x00ff0000) >> 16;
og=(lpOri[x+j+(y+i)*480] & 0x0000ff00) >> 8;
ob=lpOri[x+j+(y+i)*480] & 0x000000ff;
/* ピクセルのRGB計算 */
r+=1;
g-=2;
b-=2;
if (r>255)
r=255;
if (r>or*2)
r=or*2;
if (g<og/2)
g=og/2;
if (b<ob/2)
b=ob/2;
/* 赤みを増す */
lpBMP[x+j+(y+i)*480]=(r << 16)+(g << 8)+b;
}
}
上の例では、まず処理対象がお尻の部分に入っているかを調べ(今回使うビットマップでは、お尻以外の部分は青−0x000000ffになっている)、ピクセルのRGB成分と初期状態でのRGBを取得します。次に、赤みを増してから、RGB成分が不自然にならないように調整します。いくらなんでも赤成分のみの純色ではおかしいし、影の部分は暗くした方が良いですからね。R成分は、最初の状態の2倍を越えないように、またGB成分は最初の状態の半分を下回らないように調整します。
実際の処理では、この内側でさらに赤みを強調した領域を作って正方形の「角」を落としてみました。本当は、赤くする領域は楕円形にした方が良いんでしょうけど、角を落とした正方形でも意外にそれらしく見えますね。
角を落とすには、ピクセル処理の時の判定を以下のようにして角の部分では処理を行わないようにします。
if (x+j>=0 && x+j<480 && y+i>=0 && y+i<320 &&
lpBMP[x+j+(y+i)*600]!=255 && i*i+j*j<2200)
この辺りのどのくらい赤みを増すか、また赤みを増す領域をどうするか、という点を変えるとかなり印象も違ってきますので、いろいろいじってみると面白いでしょう。
なお自動処理中には、まず最初に手を描いてからウエイトを入れる事でたたかれた場所に一定時間手を表示しています。
自動的にたたく処理は、タイマーで行います。今回は、一つのボタンに自動/停止両方の機能をつけて、ボタンの文字列を状況によって変えてみました。
case 0: /* 自動/停止ボタン */
if (fSpanking) { /* 自動進行中 */
KillTimer(hwnd,1); /* タイマーストップ */
EnableWindow(hwReset,TRUE);
SetWindowText(hwAuto,"自動");
fSpanking=FALSE;
} else {
SetTimer(hwnd,1,1000,NULL); /* タイマーセット */
EnableWindow(hwReset,FALSE);
SetWindowText(hwAuto,"停止");
fSpanking=TRUE;
}
break;
タイマーメッセージの処理では、まず「カウンタ」を作成してこのカウンタに数値を足していきます。そして、カウンタが一定以上になったらお尻をたたいてカウンタをゼロに戻す。たたく周期を乱すために足す値は乱数で振ってやると自然になりますね。
case WM_TIMER: /* タイマーメッセージの処理 */
dwCount+=3+rand() % 8;
if (dwCount<50)
break;
hx=30+rand() % 400;
hy=20+rand() % 240;
while (lpBMP[hx+hy*480]==255) {
hx=30+rand() % 400;
hy=20+rand() % 240;
}
/* お尻たたき */
spank(hx,hy);
dwCount=0;
break;
「クリア」ボタンは、最初の状態に戻す処理を行います。これは、予めビットマップ作成の時に保存しておいた初期状態(lpOri)をビットマップにコピーして回数カウンタをリセットするだけ。
case 1: /* クリアボタン */
/* 初期状態のビットマップをコピー */
CopyMemory(lpBMP,lpOri,480*320*4);
dwSpank=0; /* 回数カウンタクリア */
InvalidateRgn(hwnd,NULL,FALSE);
UpdateWindow(hwnd);
break;
サウンドは、最初にWAVEファイルをバッファに読み込んでおいてバッファのサウンドイメージをsndPlaySound()で再生するようにしました。サウンドをメモリに置いておく事で、ディスクアクセスがなくなり再生時のもたつきを防ぐ事が出来ます。
/* バッファのサウンドイメージ再生 */ sndPlaySound(lpSp[rand() % 2],SND_ASYNC|SND_MEMORY);
ちなみにspankとは、英語で「お尻をたたく」という意味です。進行形のspankingになると「お尻たたき」の意味になります。