【開発ネタシリーズ】で開発仕様やイメージは紹介しているので、いよいよコーディングを紹介します。
1.リソース
使用するリソースはコントロール上で右クリックした場合のポップアップメニュー、1~6までのサイコロのビットマップとサイコロを一個台や机の上で転がしたWAVE音源です。ビットマップとWAVファイルはウェブ上でフリー素材を見つけました。
BCCFormはWAVE音源をカバーしていないので、「手書き」で追加します。書き方はイメージファイルと同じですね。
//-----------------------------------------
// BCCForm Ver 2.41
// An Easy Resource Editor for BCC
// Copyright (c) February 2002 by ysama
//-----------------------------------------
#include "ResDiceCtrl.h"
//--------------
// Popupメニュー
//--------------
IDM_POPUP MENU DISCARDABLE
{
POPUP "Popup(&P)"
{
MENUITEM "Roll (&R)", IDM_ROLL
}
}
//--------------------------
// イメージ(IDI_D1)
//--------------------------
IDI_D1 BITMAP DISCARDABLE "D1.bmp"
//--------------------------
// イメージ(IDI_D2)
//--------------------------
IDI_D2 BITMAP DISCARDABLE "D2.bmp"
//--------------------------
// イメージ(IDI_D3)
//--------------------------
IDI_D3 BITMAP DISCARDABLE "D3.bmp"
//--------------------------
// イメージ(IDI_D4)
//--------------------------
IDI_D4 BITMAP DISCARDABLE "D4.bmp"
//--------------------------
// イメージ(IDI_D5)
//--------------------------
IDI_D5 BITMAP DISCARDABLE "D5.bmp"
//--------------------------
// イメージ(IDI_D6)
//--------------------------
IDI_D6 BITMAP DISCARDABLE "D6.bmp"
//--------------------------
// サウンド(IDS_DICE)
//--------------------------
IDS_DICE WAVE "Dice.wav"
リソースのヘッダーファイルは以下の通り。WAVE音源は文字列のまま使うので、整数値は与えていません。
//-----------------------------------------
// BCCForm Ver 2.41
// Header File for Resource Script File
// Copyright (c) February 2002 by ysama
//-----------------------------------------
//---------------------
// メニューリソース
//---------------------
#define IDM_ROLL 100
//---------------------
// イメージリソース
//---------------------
#define IDI_D1 200
#define IDI_D2 201
#define IDI_D3 202
#define IDI_D4 203
#define IDI_D5 204
#define IDI_D6 205
2.ヘッダーファイル
先ずDLLを作る際に必要な(前にも使った)DLL.hを使います。
/////////////////////////////////////////////////////////////////////////////////////////
// DLL.h for DLL Programs
//https://docs.microsoft.com/ja-jp/cpp/cpp/definitions-and-declarations-cpp?view=msvc-170
/////////////////////////////////////////////////////////////////////////////////////////
#define EXPORT extern "C" WINAPI __declspec(dllexport)
#define IMPORT extern "C" WINAPI __declspec(dllimport)
DLL固有のヘッダーファイルは次の通り。あっさりしていますね。
//////////////////////////////////////////
// DiceCtrl.h
// Copyright (c) 05/17/2022 by BCCSkelton
//////////////////////////////////////////
//Windows用ヘッダー
#include <windows.h>
//リソースIDのヘッダー
#include "ResDiceCtrl.h"
//定数定義
#define DM_ROLL WM_USER + 1 //Roll実行メッセージ(カスタムコントロールなのでWM_USERを使う)
#define SIZE 128 //ダイスサイズ既定値
先ずBCCSkeltonを使っていないのでWindowsプログラミングに必須のwindows.h(だけ)を使っています。
後は定数定義だけですが、"DICE"コントロール(ウィンドウクラス)固有のメッセージとしてDM_ROLLをWM_USER領域で規定しています。
3.DLLMain、DiceProcコールバック、InitDiceCtrl各関数
最後に(これもあっさりしていますが)、エントリーポイントとなるDLLMain関数、このウィンドウクラス(==ダイアログコントロール)共用のコールバック関数(DiceProc)と初期化関数(InitDiceCtrl)を解説します。
//////////////////////////////////////////
// DiceCtrl.cpp
//Copyright (c) 05/17/2022 by BCCSkelton
//////////////////////////////////////////
#include "DiceCtrl.h"
#include "Dll.h"
//(解説:↑で紹介したヘッダーを読み込みます。)
///////////
//外部変数
///////////
HINSTANCE g_hInstance;
HBITMAP g_hDice[6];
int g_Dice = 0;
//(解説:上から子のDLLのインスタンス、サイコロ画像のビットマップハンドル配列、サイコロの目の整数です。)
/////////////////
// DLLMain関数
////////////////
int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpReserved) {
//インスタンスの取得
g_hInstance = hInstance;
return TRUE;
}
//(解説:ここですることは、リソースを読み込む為にDLLのインスタンスを記録することです。)
/////////////////////////
//"DICE"コールバック関数
/////////////////////////
LRESULT CALLBACK DiceProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
//(解説:このコールバック関数は(ウィンドウクラス)"DICE"コントロールが全て飛んでくる(従って引数はそれぞれ異なる)共用のコールバック関数です。それぞれのコントロールで処理は異なっても、処理の枠組みは共通です。)
//変数宣言
LONG style;
LONG estyle;
//(解説:この変数はウィンドウスタイルを変えるために必要です。)
RECT rec;
//(解説:これはコントロールの外寸、内寸を取るために必要です。)
HMENU hMenu, hPopupMenu;
//(解説:コントロール状で右マウスクリックでポップアップメニューを出すために必要です。)
POINT pt;
//(解説:同上。)
PAINTSTRUCT ps;
HDC hDC, hCompDC;
//(解説:コントロール全体にサイコロ画像を描画する為に必要です。)
//メッセージ処理
switch(Msg) {
case WM_CREATE:
//乱数の初期化
srand(time(NULL));
//(解説:乱数の初期化の定番です。)
//ダイスの目のイメージを読み込む
for(int i = 0; i < 6; i++) {
g_hDice[i] = (HBITMAP)LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_D1 + i), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE);
if(!g_hDice[i])
MessageBox(hWnd, "ダイスのビットマップが読めません", "エラー", MB_OK | MB_ICONERROR);
}
//(解説:サイコロの目の画像のID整数を並べているのでこのような処理ができます。)
break;
case WM_RBUTTONDOWN:
//ポップアップメニューの読込
hMenu = LoadMenu(g_hInstance, "IDM_POPUP");
hPopupMenu = GetSubMenu(hMenu, 0);
//ウインドウの位置情報を取得
GetWindowRect(hWnd, &rec);
//ポップアップメニューの表示場所を設定
GetCursorPos(&pt);
//ポップアップメニューの表示
TrackPopupMenu(hPopupMenu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON, pt.x, pt.y, 0, hWnd, &rec);
//ポップアップメニューの開放
DestroyMenu(hMenu);
//(解説:マウス右クリックによるポップアップメニューの定番処理です。)
break;
case WM_COMMAND:
if(LOWORD(wParam) != IDM_ROLL)
return DefWindowProc(hWnd, Msg, wParam, lParam);
//(解説:ポップアップメニューの"IDM_ROLL"(それしかないが)が押された場合に下に処理が流れます。)
case WM_LBUTTONDOWN:
//(解説:マウス左クリックの場合下に流れますので、ポップアップメニューと合流してサイコロ音を鳴らします。)
PlaySound("IDS_DICE", g_hInstance, SND_RESOURCE | SND_ASYNC);
case DM_ROLL:
//(解説:固有のメッセージDM_ROLLの場合にはサイコロ音はなりませんが、ここから全て処理が共通となります。)
//ウィンドウを押し下げたようにする
style = GetWindowLong(hWnd, GWL_STYLE) ^ WS_DLGFRAME;
SetWindowLong(hWnd, GWL_STYLE, style);
estyle = GetWindowLong(hWnd, GWL_EXSTYLE) ^ WS_EX_WINDOWEDGE;
//(解説:ウィンドウ(拡張)スタイルを変更します。XORでビットを消すのも定番ですね。)
estyle |= WS_EX_CLIENTEDGE;
//(解説:ウィンドウ拡張スタイルを変更します。赤と水色のスタイルが一番「高低差」を感じさせます。)
SetWindowLong(hWnd, GWL_EXSTYLE, estyle);
SetWindowPos(hWnd, NULL, 0, 0, 0, 0, (SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED));
//(解説:変更したスタイルでフレーム迄再描画します。)
//乱数でサイコロの目を決める
for(int i = 0; i < 10; i++) {
g_Dice = rand() % 6; //m_Dice = 0 - 5
SetProp(hWnd, "DICE_NUM", (HANDLE)g_Dice); //そのウィンドウにサイコロの目を保存する
InvalidateRect(hWnd, NULL, TRUE); //領域無効化
UpdateWindow(hWnd); //再描画命令
Sleep(50);
//(解説:乱数で0~5のサイコロの目を求め、各"DICE"コントロールに「埋め込み」、再描画して待機します。)
}
//ウィンドウを元に戻す
style |= WS_DLGFRAME;
SetWindowLong(hWnd, GWL_STYLE, style);
estyle |= WS_EX_WINDOWEDGE;
estyle ^= WS_EX_CLIENTEDGE;
SetWindowLong(hWnd, GWL_EXSTYLE, estyle);
SetWindowPos(hWnd, NULL, 0, 0, 0, 0, (SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED));
//(解説:変更したスタイルを元に戻しフレーム迄再描画します。)
//親へWin3.xのオマージュで通知コードHIWORD(wParam)の代わりに賽の目のメッセージを送る
SendMessage(GetParent(hWnd), WM_COMMAND, MAKELONG(GetDlgCtrlID(hWnd), g_Dice + 1), LPARAM(hWnd));
//(解説:WM_COMMANDを使った親への通知です。サイコロの目を返す処理一つしかないのでこれにしましたが、コントロールの通知コードが必要な場合には使えないことにご注意ください。)
//親からのDM_ROLLメッセージの戻り値として賽の目を返す
return g_Dice + 1;
//(解説:親から"SendMessage(hDiceCtrl, DM_ROLL, 0, 0);"で呼ばれた時の戻り値です。)
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
//ダイスの描画
//クライアントエリアサイズの取得
RECT rec;
GetClientRect(hWnd, &rec);
//ウィンドウプロパティからサイコロの目を取り出す
g_Dice = (int)GetProp(hWnd, "DICE_NUM");
//ビットマップの描画
BITMAP bmp;
GetObject(g_hDice[g_Dice], sizeof(BITMAP), &bmp);
hCompDC = CreateCompatibleDC(hDC);
SelectObject(hCompDC, g_hDice[g_Dice]);
StretchBlt(hDC, 0, 0, rec.right - rec.left, rec.bottom - rec.top, hCompDC, 0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);
DeleteDC(hCompDC);
//(解説:ここがコントロールのクライアントエリア一杯にサイコロの目の画像を描画する処理です。switch内の変数宣言はできないはずですが、赤のコードがエラーが出ずに残ってしまいましたので、本日修正しました。修正版のアップは次の機会に(汗;)しかし、全く同じ変数名がswitch文の中と外で共存できる、というのがC++的ですね。ん、BITMAP変数も宣言できている?はて、面妖な。まっ、いいか。)
EndPaint(hWnd, &ps);
break;
case WM_CLOSE:
//ダイスの目のイメージを破棄する
for(int i = 0; i < 6; i++)
DeleteObject((HGDIOBJ)g_hDice[i]);
//ウィンドウプロパティを削除する
RemoveProp(hWnd, "DICE_NUM");
//(解説:お忘れなく。メモリーリークの原因となります。)
DestroyWindow(hWnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, Msg, wParam, lParam);
}
return 0L;
}
EXPORT bool InitDiceCtrl(HINSTANCE hInstance) {
//(解説:やっとここでEXPORTが出てきました。そうです、DLL外部で使う関数はこれ一つなんです。コモンコントロールのInitCommanControl()関数も似たようなことをやっているのかしら?)
//"DICE"ウィンドウクラスの登録
WNDCLASSEX WndClass;
//ウィンドウクラスの登録
WndClass.cbSize = sizeof(WNDCLASSEX); //この構造体のサイズ
WndClass.style = CS_HREDRAW | CS_VREDRAW; //縦横移動、サイズ変更時再描画
WndClass.lpfnWndProc= DiceProc; //コールバック関数ポインター
WndClass.cbClsExtra = 0; //この構造体以降に置くバイト数
WndClass.cbWndExtra = 0; //インスタンス以降に置くバイト数
WndClass.hInstance = hInstance; //このウィンドウのインスタンス
WndClass.hIcon = NULL; //このウィンドウクラスのアイコン
WndClass.hCursor = LoadCursor(NULL, IDC_ARROW); //このウィンドウクラスのカーソル
WndClass.hbrBackground = (HBRUSH)WHITE_BRUSH; //このウィンドウクラスの背景色
WndClass.lpszMenuName = NULL; //このウィンドウクラスのメニュー
WndClass.lpszClassName = "DICE"; //このウィンドウクラスの名称
WndClass.hIconSm = NULL; //最小化時の小さいアイコン
if(!RegisterClassEx(&WndClass)) {
MessageBox(NULL, "ウィンドウを登録できませんでした。", "エラー", MB_OK | MB_ICONEXCLAMATION);
return FALSE;
}
return TRUE;
}
//(解説:このウィンドウ登録処理のみが外部へ解放されています。また、"DICE"クラスウィンドウが作成される前に登録する必要があることは自明ですね。CPICBOXクラスでは位置が固定できるstaticのダミー関数を作り、その後そのダミー関数からインスタンス固有の子―^るバック関数を呼ぶようにしましたが、DLLベースのウィンドウクラスではコールバック関数を共用とせざるを得ないので、固有の処理はウィンドウ毎のIDで区別する必要があります。)
4.結語
以上でDLLを使った「サイコロコントロール」ができました。子の作成過程を見ると、例えばボタンコントロール("BUTTON")はWM_LBUTTONDOWNの際に「浮き上がったボタンの画像」を「押し下げたボタンの画像」に交換し、画像の中に文字を書いて(またはイメージを描画して)、WM_LBUTTONUPの際にそれを元に戻す、という処理をしているのだな、ということがよく分かります。別の言葉で言えば、DLLを使うことにより(今回は「押し下げられたように見せ→サイコロ画像を10回変更し→押し上げられた画像に戻して、最後の画像番号+1を返す」という簡単な処理でしたが、同様に)「ウィンドウクラスを登録する」+「ウィンドウ毎に区別して処理を行う共用コールバック関数を作る」+「処理の結果を呼び出し元や親へ送信する」という機能を備えれば、どのような自作コントロールもできるということです。
貴方も貴方だけのダイアログコントロールを作ってみませんか?