さてさて、正月ネタに「ハノイの塔」を取り上げ、C#(コンソール)、C++(コンソール)、BCCSkelton(ウィンドウズ)とプログラミングしてきて、とうとう当初のイメージに到達しました。仕上げは今まで書いてきた内容の総括としてTowerOfHanoiProc.hの概略を「//解説:」で解説します。
【TowerOfHanoiProc.h】
//////////////////////////////////////////
// TowerOfHanoiProc.h
// Copyright (c) 01/13/2023 by BCCSkelton
//////////////////////////////////////////
/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnInit(WPARAM wParam, LPARAM lParam) {
//石盤段数選択コンボボックスの初期化
char num[2] = {'1', 0};
for(int i = 0; i < 9; i++) {
SendItemMsg(IDC_INIT, CB_ADDSTRING, 0, (LPARAM)num); //1~9をセットする
num[0]++;
//解説:これは'1'~'9'までの数字文字をコンボボックスに登録する処理ですが、"1(NULL)"の文字列を作り、'1'の部分をインクリメントして作っています。
}
//PICTUREBOXの初期化
m_PicBox.Init(m_hWnd, IDC_SCREEN); //ダイアログコントロールIDC_SCREENにラップ
//解説:(これはCPICBOXクラスを確認していただきたいのですが)親ハンドルとコントロールIDで初期化します。
RECT rec; //m_PicBoxのクライアントエリアサイズを取得
GetClientRect(m_PicBox.m_hWnd, &rec);
m_Width = rec.right - rec.left; //m_PicBoxクライアントエリアの幅(500)
m_Height = rec.bottom - rec.top; //m_PicBoxクライアントエリアの高さ(499)
//解説:ビットマップは500 x 500ですが、リソースファイルの値をいくら調整しても切り捨てにより499になってしまいます。
//m_PicBoxの背景にビットマップをPsetで貼り付ける
m_BkGrnd = (HBITMAP)LoadImage(m_hInstance, MAKEINTRESOURCEA(IDI_BACKGRND), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADMAP3DCOLORS);
m_PicBox.PutBMP(m_BkGrnd, 0, 0, 0);
//解説:500 x 500の背景を取得して貼り付けます。
//背景に柱(w16 x h320)を3本((117, 135)、(242, 135)、(367, 135))描く
COLORREF col = m_PicBox.m_Color; //現在の選択色を保存
m_PicBox.m_Color = RGB(115, 57, 57); //濃い茶色で描画
//柱座標の記録と柱描画
int mergin = (m_Width - DISKWIDTH * 3) / 4; //石盤間の間隔
for(int i = 0; i < 3; i++) { //柱中心のx座標と頂点のy座標を記録
m_Pole[i].m_y = (m_Height - DISKHEIGHT * MAXTIRE) * 2 / 3;
m_Pole[i].m_x = mergin + DISKWIDTH / 2 + DISKWIDTH * i;
//解説:↑はx、y座標の記録です。
m_PicBox.Box((m_Pole[i].m_x - 8), m_Pole[i].m_y, (m_Pole[i].m_x + 8), m_Pole[i].m_y + DISKHEIGHT * MAXTIRE, 1);
//解説:これが棒(長方形)描画で、Basicに似せたBox関数を使います。
}
m_PicBox.m_Color = col; //選択色を元に戻す
//石盤ビットマップの読み込み
for(int i = 0; i < MAXTIRE; i++) { //石盤0~8迄
m_Disk[i].Init(&m_PicBox, IDI_DISK1 + i, IDI_MASK1 + i);
}
//解説:石盤イメージは描画ビッットマップ、マスクビットマップ共に一度に読み込みます。
return TRUE;
}
bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {
if(MessageBox(m_hWnd, "終了しますか", "終了確認",
MB_YESNO | MB_ICONINFORMATION) == IDYES) {
//背景ビットマップの開放
DeleteObject(m_BkGrnd);
//解説:使用したビットマップは開放しなければなりません。盤のビットマップは「盤」クラスのデストラクターで開放します。
//処理をするとDestroyWindow、PostQuitMessageが呼ばれる
return TRUE;
}
else
//そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
return FALSE;
}
//ダイアログベースの場合はこれが必要
bool CMyWnd::OnDestroy(WPARAM wPram, LPARAM lParam) {
PostQuitMessage(0);
//解説:ウィンドウベースの場合、DefWindowProc(Default Window Procedureのことだと思います)がWM_CLOSEを受け付けるとWM_DESTROYメッセージを出しますが、ダイアログはユーザーで手配する必要があります。
return TRUE;
}
/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////
bool CMyWnd::OnInit(WPARAM wParam) {
if(HIWORD(wParam) == CBN_SELCHANGE) {
//解説:コンボボックスのエディットボックスの初期値は「未選択」なので、盤の段数をユーザーは必ず入力しなければなりません。その際に出されるメッセージがCBN_SELCHANGEで、設定や設定変更がある都度以下が実行されます。
//柱情報の初期化
for(int i = 0; i < 3; i++)
m_Pole[i].Init();
//解説:「柱」クラスのオブジェクトを初期化関数で初期化します。
//描画済の石盤を消す
if(m_Tire) {
for(int i = 0; i < m_Tire; i++)
m_Disk[i].Erase();
//解説:すでに石盤の段数が設定されていたならば、石盤のイメージを消去します。
}
//指定の石盤数を取得する
m_Tire = SendItemMsg(IDC_INIT, CB_GETCURSEL, 0, (LPARAM)0) + 1;
//解説:コンボボックスから('1'ベースの)段数を取得します。
char str[MAX_PATH];
wsprintf(str, "ハノイの塔が初期化され、段数が %d段に設定されました。", m_Tire);
MessageBox(m_hWnd, str, "初期化メッセージ", MB_OK | MB_ICONINFORMATION);
//解説:開始時のメッセージを表示します。
//最初の柱に石盤を設置する
for(int i = m_Tire - 1; i >= 0; i--) {
m_Pole[0].Push(i);
m_Disk[i].Show(m_Pole[0].m_x - m_Disk[i].m_w / 2, m_Pole[0].m_y + DISKHEIGHT * (MAXTIRE - m_Tire + i));
//解説:一番左の柱(fromPole)に石盤の数だけPushを行うことで石盤(の初期状態)が表示されます。緑字の部分は、x座標については柱の中心座標(m_Pole[0].m_x)から石盤の幅(m_Disk[i].m_w)の半分を引いて真ん中に合わせ、y座標については柱の上端(m_Pole[0].m_y)に石盤の厚さ(DISKHEIGHT)を石盤の段数で乗じた全長(MAXTIRE=下端)から、幅の広い方から上に(MAXTIRE - m_Tire + i は MAXTIRE - m_Tire + (m_Tire - 1) = MAXTIRE - 1で始まり、MAXTIRE - m_Tireで終わります)描画して行きます。
}
return TRUE;
}
else
return FALSE;
}
bool CMyWnd::OnStart() {
//解説:解説を書いていて、エラーの際にボタンがDisableのままになる不具合を見つけたので赤字で修正を加えています。
//戻り値変数
bool ret = TRUE;
//ボタンを使用不可にする
EnableWindow(GetDlgItem(m_hWnd, IDC_START), FALSE);
EnableWindow(GetDlgItem(m_hWnd, IDOK), FALSE);
//解説:「ハノイの塔」を実行中にFocusを失ったり、割込みをかけるとほぼ間違いなく落ちますので、割込みを禁止する意味でDisableの状態にします。
//段数選択状態の確認
int num = SendItemMsg(IDC_INIT, CB_GETCURSEL, 0, (LPARAM)0);
//未選択の場合のエラー処理
if(num == CB_ERR) {
MessageBox(m_hWnd, "段数が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
ret = FALSE;
}
else {
TowerofHanoi(m_Tire, 0, 1, 2);
MessageBox(m_hWnd, "「ハノイの塔」を終了しました。", "終了メッセージ", MB_OK | MB_ICONINFORMATION);
//描画済の石盤を消す
for(int i = 0; i < m_Tire; i++)
m_Disk[i].Erase();
//柱情報の初期化
for(int i = 0; i < 3; i++)
m_Pole[i].Init();
SendItemMsg(IDC_INIT, CB_SETCURSEL, -1, (LPARAM)0); //未選択状態にする
}
//ボタンを使用可にする
EnableWindow(GetDlgItem(m_hWnd, IDC_START), TRUE);
EnableWindow(GetDlgItem(m_hWnd, IDOK), TRUE);
return ret;
}
bool CMyWnd::OnIdok() {
SendMsg(WM_CLOSE, 0, 0);
//解説:自分にWM_CLOSEメッセージを発行します。
return TRUE;
}
///////////////////
//ユーザー定義関数
///////////////////
int CMyWnd::Pop(int pole) {
int disk = m_Pole[pole].Pop();
m_Disk[disk].Erase();
UpdateWindow(m_PicBox.m_hWnd);
return disk;
//解説:これは前回説明しました。赤で「柱」と「盤」のデータを更新し、青で消去しています。
}
void CMyWnd::Push(int pole, int disk) {
int tire = m_Pole[pole].Push(disk) + 1;
m_Disk[disk].Show(m_Pole[pole].m_x - m_Disk[disk].m_w / 2, m_Pole[pole].m_y + DISKHEIGHT * (MAXTIRE - tire));
Sleep(WAIT);
//解説:これは前回説明しました。赤で「柱」と「盤」のデータを更新し、青で表示しています。
}
void CMyWnd::Move(int fromPole, int toPole) {
//先にデータを更新する
int disk = m_Pole[fromPole].Pop();
int tire = m_Pole[toPole].Push(disk) + 1;
//データに沿って石盤を動かす
int top = m_Pole[0].m_y - 32; //柱から抜けた所
int x = m_Disk[disk].m_x, y = m_Disk[disk].m_y;
while(y >= top) { //top迄上に動かす
m_Disk[disk].Erase();
UpdateWindow(m_PicBox.m_hWnd);
y--;
m_Disk[disk].Show(x, y);
UpdateWindow(m_PicBox.m_hWnd);
}
int goalx = m_Pole[toPole].m_x - m_Disk[disk].m_w / 2;
if(x <= goalx) {
while(x <= goalx) { //右に動かす
m_Disk[disk].Erase();
UpdateWindow(m_PicBox.m_hWnd);
x++;
m_Disk[disk].Show(x, y);
UpdateWindow(m_PicBox.m_hWnd);
}
}
else {
while(x >= goalx) { //左に動かす
m_Disk[disk].Erase();
UpdateWindow(m_PicBox.m_hWnd);
x--;
m_Disk[disk].Show(x, y);
UpdateWindow(m_PicBox.m_hWnd);
}
}
y = y;
int goaly = m_Pole[toPole].m_y + DISKHEIGHT * (MAXTIRE - tire);
while(y <= goaly) { //下に動かす
m_Disk[disk].Erase();
UpdateWindow(m_PicBox.m_hWnd);
y++;
m_Disk[disk].Show(x, y);
UpdateWindow(m_PicBox.m_hWnd);
}
//解説:Pop、Pushのコードと比較してください。赤で「柱」と「盤」のデータを更新し、青で表示し、緑で移動しています。
}
//「ハノイの塔」メイン関数
void CMyWnd::TowerofHanoi(int diskCount, int fromPole, int toPole, int viaPole) {
if(diskCount > 0) {
TowerofHanoi(diskCount - 1, fromPole, viaPole, toPole);
//ハノイの塔を描画する(石盤移動無し)
// int num = Pop(fromPole);
// Push(toPole, num);
//MessageBox(m_hWnd, "In TowerofHanoi", "Check", MB_OK | MB_ICONINFORMATION); //毎回の移動を確認する場合これを使ってください。
//ハノイの塔を描画する(石盤移動)
Move(fromPole, toPole);
TowerofHanoi(diskCount - 1, viaPole, toPole, fromPole);
}
//解説:最初コンソール版と同じ動きをするPop、Pushでのコードで書いて、その後石盤の移動状態も表示するMoveに書き換えましたので、2刀流になっています。Pop、Pushでの動作も確認してください。
}
いつものことですが、今まででできた6つのファイルのうち、リソースファイルとcppファイル
TowerOfHanoi.rc
ResTowerOfHanoi.h
TowerOfHanoi.bdp
TowerOfHanoi.h
TowerOfHanoiProc.h
TowerOfHanoi.cpp
をBatchGood.exeにドロップして、オプションの「詳細」で「Windowsアプリケーション」「インクルードパス(BCCSkelton.hのあるフォールダー)」を指定してからコンパイルしてください。Pop、Pushではウェイトを入れましたが、Moveではそのままで良いようです。(もっとゆっくり見たいという場合にはSleep(miliseconds)を入れてください。)
文字列を使わないアプリなら、速度、サイズ共にBCCSkeltonが良いようです。最初はそこまでやる気はありませんでしたが、結局「僕ならば...」仕様でのアプリまで作っちゃいました。久々にBCCSkeltonを使いましたが、矢張り手になじんでいるので簡単に作れますね。満足、満足。