前回発作的にC#でMDIのスケルトンを作りました(注)が、子ウィンドウにウェブブラウザーを貼り付けると結構面白いかも、と考え、「VisualStudioで各種言語の64bitコンパイラーをフリーで提供する、今や太っ腹なMicrosoftさんなら何かコントロールがあるはず?」ということでググってみました。

 

昔はIE(Internet Explorer 11)ベースのWebBrowserクラスがあったということですが、色々と問題ありで、現在Edgeが依拠する表題のWebView2クラスというものがあるそうです。(注)

注:[C#] WebBrowserコントロールからWebView2への切り替え | OsadaSoftWebBrowserからWebView2への切り替え方法|プログラムでネットサーフィン (biz-prog.net)WinForms アプリでの WebView2 の概要

 

なので、やるならWebView2なのですが、これを使うには単に「Win 11でMicrosof Edgeが使えている環境」では不足であり、別途専用サイトからPCに合わせたプログラミングプラットフォームを落とすことが必要だそうです。なお、プラットフォームはx86、x64とRISCのARM64があるようです。

 

一応x64ベースのもの(注)を落としましたが、矢張り時期尚早感がぬぐえず、またMicrosoft Learnでも使用にあたってはVisual Studioの利用が前提であり、取り敢えず現在はまだ展開せずにそのままの状態にしています。

注:Microsoft.WebView2.FixedVersionRuntime.110.0.1587.41.x64.cab、205MB

 

64bitプログラムコンパイラーとしてのC#とその柔軟性や豊富なライブラリーや拡張性には敬服しますが、どうも段々と私には追い付けなくなりつつあるようで、またEmbarcaderoのC++コンパイラーも32bit以上のものは出そうになく、私のプログラミング寿命は最終期に近づいてきつつあるのかもしれません。

 

取り敢えず、今は次のネタを追いかけて、「老骨」ならず「老脳みそ」に鞭打って、次のネタを考えてみましょう。

 

ps. なお、MSCompAssのサンプルにFormSDISkeltonとFormMDISkeltonを追加しておきました。

 

BCCSkeltonで作った「ハノイの塔(TowerOfHanoi.exe)」がきちんと動いてくれているので満足していますが、またまた次のネタに窮してしまったので、暇に任せて(今度は折角覚えたC#を忘れないようにと)MSCompAssを使ってSDIとMDIのスケルトンを作ってみました。

 

SDIのスケルトンはBCCFormandBCCSkeltonパッケージのMSCompAssにサンプルとして載っているResReader.csとほぼ変わらないので、今回はMDIのサンプルのみ載っけます。何にもしないソフトなので役に立ちませんがご参考迄。(なお、リソースはシステムアイコンを別建でコンパイルしているので、FormMDISkelton.resourcesというリソースファイルには、↓のイメージの通り(BCCFormツールのTBEditorで作った16 x 15の)ツールバービットマップのみ入れています。)

 

【FormMDISkelton.cs】

//////////////////////
// MDISkelton.cs
//////////////////////
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Reflection;        //Assemblyを使う為
using System.Resources;            //リソース関係クラス等の使用の為
using System.IO;                //Stream関係クラス等の使用の為

///////////////////////////
//エントリーポイントクラス
///////////////////////////
class MainApp
{
    [STAThread]
    public static void Main()
    {
        Application.Run(new MainWnd());
    }
}

///////////////////////
//メインフォームクラス
///////////////////////
public partial class MainWnd : Form
{
    //クラスメンバー変数
    ToolStrip toolStrip;                        //ツールバー
    ToolStripButton[] toolStripButton;            //ツールバーボタン
    StatusStrip statusStrip;                    //ステータスバー
    ToolStripStatusLabel[] tssl;                //ステータスバーラベル

    public MainWnd()
    {
        Assembly myOwn = Assembly.GetEntryAssembly();
        this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);    //プログラムアイコンをフォームにつける
        this.Load += new EventHandler(MainForm_Load);
        this.Text = "MainWindow Name";
        this.ClientSize = new Size(640, 480);
        this.MinimumSize = new Size(480, 360);
        this.BackColor = SystemColors.Window;
        this.IsMdiContainer = true;
    }

    //WM_CREATE時処理
    private void MainForm_Load(object sender, EventArgs e)
    {
        //メニュー作成
        SetMenu();
        //ツールバーとステータスバー等コントロール作成
        SetControls();
    }

    //WM_CLOSE時処理
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        base.OnFormClosing(e);
        DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.No)
        {
            e.Cancel = true;
        }
        //開いている子ウィンドウを総て閉じる
        foreach(Form frm in this.MdiChildren)
        {
               frm.Close();
        }
    }

    //メニューの設定
    protected void SetMenu()
    {
        //メインメニュー作成
        MainMenu menu = new MainMenu();
        Menu = menu;
        //メニューアイテム付加
        MenuItem miFile = new MenuItem();        //「ファイル」メニュー
        miFile.Text = "ファイル(&F)";
        miFile.Index = 0;
        menu.MenuItems.Add(miFile);
        MenuItem miEdit = new MenuItem();        //「編集」メニュー
        miEdit.Text = "編集(&E)";
        miEdit.Index = 1;
        menu.MenuItems.Add(miEdit);
        MenuItem miWnd = new MenuItem();        //「ウィンドウ」メニュー
        miWnd.Text = "ウィンドウ(&W)";
        miWnd.Index = 2;
        miWnd.MdiList = true;
        menu.MenuItems.Add(miWnd);
        MenuItem miHelp = new MenuItem();        //「ヘルプ」メニュー
        miHelp.Text = "ヘルプ(&H)";
        miHelp.Index = 3;
        menu.MenuItems.Add(miHelp);
        MenuItem miNew = new MenuItem();        //「新規」メニューアイテム
        miNew.Text = "新規(&N)";
        miNew.Index = 0;
        miNew.Click += OnNew_Click;
        miNew.Shortcut = Shortcut.CtrlN;
        miFile.MenuItems.Add(miNew);
        MenuItem miOpen = new MenuItem();        //「開く」メニューアイテム
        miOpen.Text = "開く(&O)";
        miOpen.Index = 1;
        miOpen.Click += OnOpen_Click;
        miOpen.Shortcut = Shortcut.CtrlO;
        miFile.MenuItems.Add(miOpen);
        MenuItem miSave = new MenuItem();        //「保存」メニューアイテム
        miSave.Text = "保存(&S)";
        miSave.Index = 2;
        miSave.Click += OnSave_Click;
        miSave.Shortcut = Shortcut.CtrlS;
        miFile.MenuItems.Add(miSave);
        MenuItem miSaveAs = new MenuItem();        //「名前を付けて保存」メニューアイテム
        miSaveAs.Text = "名前を付けて保存(&A)";
        miSaveAs.Index = 3;
        miSaveAs.Click += OnSaveAs_Click;
        miSave.Shortcut = Shortcut.CtrlA;
        miFile.MenuItems.Add(miSaveAs);
        miFile.MenuItems.Add("-");                //セパレーター
        MenuItem miExit = new MenuItem();        //「終了」メニューアイテム
        miExit.Text = "終了(&X)";
        miExit.Index = 4;
        miExit.Click += OnExit_Click;
        miExit.Shortcut = Shortcut.CtrlX;
        miFile.MenuItems.Add(miExit);
        MenuItem miCut = new MenuItem();        //「切り取り」メニューアイテム
        miCut.Text = "切り取り(&T)";
        miCut.Index = 0;
        miCut.Click += OnCut_Click;
        miCut.Shortcut = Shortcut.CtrlT;
        miEdit.MenuItems.Add(miCut);
        MenuItem miCopy = new MenuItem();        //「コピー」メニューアイテム
        miCopy.Text = "コピー(&C)";
        miCopy.Index = 1;
        miCopy.Click += OnCopy_Click;
        miCopy.Shortcut = Shortcut.CtrlC;
        miEdit.MenuItems.Add(miCopy);
        MenuItem miPaste = new MenuItem();        //「貼り付け」メニューアイテム
        miPaste.Text = "貼り付け(&P)";
        miPaste.Index = 2;
        miPaste.Click += OnPaste_Click;
        miPaste.Shortcut = Shortcut.CtrlP;
        miEdit.MenuItems.Add(miPaste);
        miEdit.MenuItems.Add("-");                //セパレーター
        MenuItem miFont = new MenuItem();        //「フォント」メニューアイテム
        miFont.Text = "フォント(&F)";
        miFont.Index = 3;
        miFont.Click += OnFont_Click;
        miFont.Shortcut = Shortcut.CtrlF;
        miEdit.MenuItems.Add(miFont);
        miEdit.MenuItems.Add("-");                //セパレーター
        MenuItem miFind = new MenuItem();        //「検索」メニューアイテム
        miFind.Text = "検索(&F)";
        miFind.Index = 4;
        miFind.Click += OnFind_Click;
        miFind.Shortcut = Shortcut.CtrlF;
        miEdit.MenuItems.Add(miFind);
        MenuItem miReplace = new MenuItem();    //「置換」メニューアイテム
        miReplace.Text = "置換(&R)";
        miReplace.Index = 5;
        miReplace.Click += OnReplace_Click;
        miReplace.Shortcut = Shortcut.CtrlR;
        miEdit.MenuItems.Add(miReplace);
        MenuItem miCascade = new MenuItem();    //「重ねて並べる(&C)」メニューアイテム
        miCascade.Text = "重ねて並べる(&C)";
        miCascade.Index = 0;
        miCascade.Click += OnCascade_Click;
        miCascade.Shortcut = Shortcut.CtrlC;
        miWnd.MenuItems.Add(miCascade);
        MenuItem miTileVert = new MenuItem();    //「縦に並べる(&V)」メニューアイテム
        miTileVert.Text = "縦に並べる(&V)";
        miTileVert.Index = 1;
        miTileVert.Click += OnTileVert_Click;
        miTileVert.Shortcut = Shortcut.CtrlV;
        miWnd.MenuItems.Add(miTileVert);
        MenuItem miTileHorz = new MenuItem();    //「横に並べる(&H)」メニューアイテム
        miTileHorz.Text = "横に並べる(&H)";
        miTileHorz.Index = 2;
        miTileHorz.Click += OnTileHorz_Click;
        miTileHorz.Shortcut = Shortcut.CtrlH;
        miWnd.MenuItems.Add(miTileHorz);
        MenuItem miArrIcon = new MenuItem();    //「アイコンの整列(&I)」メニューアイテム
        miArrIcon.Text = "アイコンの整列(&I)";
        miArrIcon.Index = 2;
        miArrIcon.Click += OnArrIcon_Click;
        miArrIcon.Shortcut = Shortcut.CtrlI;
        miWnd.MenuItems.Add(miArrIcon);
        MenuItem miHowtoUse = new MenuItem();    //「使い方」メニューアイテム
        miHowtoUse.Text = "使い方(&U)";
        miHowtoUse.Index = 0;
        miHowtoUse.Click += OnHowtoUse_Click;
        miHelp.MenuItems.Add(miHowtoUse);
        MenuItem miVer = new MenuItem();        //「バージョン」メニューアイテム
        miVer.Text = "バージョン(&V)";
        miVer.Index = 1;
        miVer.Click += OnVersion_Click;
        miVer.Shortcut = Shortcut.CtrlV;
        miHelp.MenuItems.Add(miVer);
    }

    //ツールバーとステータスバー等コントロールの設定
    protected void SetControls()
    {
        //フォームのレイアウトを一時停止
        this.SuspendLayout();
        //ToolStripクラスインスタンスの生成
        this.toolStrip = new ToolStrip();
        //ツールバーのレイアウトを一時停止
        this.toolStrip.SuspendLayout();
        //ToolStripButton配列を作成
        this.toolStripButton = new ToolStripButton[12];
        //本プログラムの埋め込みリソースのリソースマネージャーを作成
        Assembly asm = Assembly.GetExecutingAssembly();
        ResourceManager rm = new ResourceManager("FormMDISkelton", asm);
        //ツールバービットマップの読み込み
        ImageList imgList = new ImageList();
        imgList.ImageSize = new Size(16, 15);
        imgList.Images.AddStrip((Bitmap)rm.GetObject("ToolBar"));
        imgList.TransparentColor = Color.White;
        //ToolStripButton[0]を作成
        this.toolStripButton[0] = new ToolStripButton();
        this.toolStripButton[0].Text = "新規作成(&N)";                            //テキスト設定
        this.toolStripButton[0].Image = (Bitmap)imgList.Images[0];                //画像設定
        this.toolStripButton[0].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[0].Click += OnNew_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[0]);                        //ボタンを追加
        //ToolStripButton[1]を作成
        this.toolStripButton[1] = new ToolStripButton();
        this.toolStripButton[1].Text = "開く(&O)";                                //テキスト設定
        this.toolStripButton[1].Image = (Bitmap)imgList.Images[1];                //画像設定
        this.toolStripButton[1].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[1].Click += OnOpen_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[1]);                        //ボタンを追加
        //ToolStripButton[2]を作成
        this.toolStripButton[2] = new ToolStripButton();
        this.toolStripButton[2].Text = "保存(&S)";                                //テキスト設定
        this.toolStripButton[2].Image = (Bitmap)imgList.Images[2];                //画像設定
        this.toolStripButton[2].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[2].Click += OnSave_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[2]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[3]を作成
        this.toolStripButton[3] = new ToolStripButton();
        this.toolStripButton[3].Text = "終了(&X)";                                //テキスト設定
        this.toolStripButton[3].Image = (Bitmap)imgList.Images[3];                //画像設定
        this.toolStripButton[3].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[3].Click += OnExit_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[3]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[4]を作成
        this.toolStripButton[4] = new ToolStripButton();
        this.toolStripButton[4].Text = "切り取り(&T)";                            //テキスト設定
        this.toolStripButton[4].Image = (Bitmap)imgList.Images[4];                //画像設定
        this.toolStripButton[4].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[4].Click += OnCut_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[4]);                        //ボタンを追加
        //ToolStripButton[5]を作成
        this.toolStripButton[5] = new ToolStripButton();
        this.toolStripButton[5].Text = "コピー(&C)";                            //テキスト設定
        this.toolStripButton[5].Image = (Bitmap)imgList.Images[5];                //画像設定
        this.toolStripButton[5].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[5].Click += OnCopy_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[5]);                        //ボタンを追加
        //ToolStripButton[6]を作成
        this.toolStripButton[6] = new ToolStripButton();
        this.toolStripButton[6].Text = "貼り付け(&P)";                            //テキスト設定
        this.toolStripButton[6].Image = (Bitmap)imgList.Images[6];                //画像設定
        this.toolStripButton[6].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[6].Click += OnPaste_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[6]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[7]を作成
        this.toolStripButton[7] = new ToolStripButton();
        this.toolStripButton[7].Text = "フォント(&F)";                            //テキスト設定
        this.toolStripButton[7].Image = (Bitmap)imgList.Images[7];                //画像設定
        this.toolStripButton[7].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[7].Click += OnFont_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[7]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[8]を作成
        this.toolStripButton[8] = new ToolStripButton();
        this.toolStripButton[8].Text = "検索(&F)";                                //テキスト設定
        this.toolStripButton[8].Image = (Bitmap)imgList.Images[8];                //画像設定
        this.toolStripButton[8].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[8].Click += OnFind_Click;                            //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[8]);                        //ボタンを追加
        //ToolStripButton[9]を作成
        this.toolStripButton[9] = new ToolStripButton();
        this.toolStripButton[9].Text = "置換(&R)";                                //テキスト設定
        this.toolStripButton[9].Image = (Bitmap)imgList.Images[9];                //画像設定
        this.toolStripButton[9].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[9].Click += OnReplace_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[9]);                        //ボタンを追加
        //セパレーターを挿入
        this.toolStrip.Items.Add(new ToolStripSeparator());
        //ToolStripButton[10]を作成
        this.toolStripButton[10] = new ToolStripButton();
        this.toolStripButton[10].Text = "使い方(&U)";                            //テキスト設定
        this.toolStripButton[10].Image = (Bitmap)imgList.Images[10];            //画像設定
        this.toolStripButton[10].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[10].Click += OnHowtoUse_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[10]);                        //ボタンを追加
        //ToolStripButton[11]を作成
        this.toolStripButton[11] = new ToolStripButton();
        this.toolStripButton[11].Text = "バージョン情報(&V)";                    //テキスト設定
        this.toolStripButton[11].Image = (Bitmap)imgList.Images[11];            //画像設定
        this.toolStripButton[11].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
        this.toolStripButton[11].Click += OnVersion_Click;                        //Clickイベントハンドラ追加
        this.toolStrip.Items.Add(this.toolStripButton[11]);                        //ボタンを追加
        //ツールバーの設定
        this.Controls.Add(this.toolStrip);
        //ツールバーのレイアウトを再開
        this.toolStrip.ResumeLayout(false);
        this.toolStrip.PerformLayout();

        //StatusStripクラスインスタンスの生成
        this.statusStrip = new StatusStrip();
        //ステータスバーにパネルとテキストを追加
        tssl = new ToolStripStatusLabel[3];
        tssl[0] = new ToolStripStatusLabel();
        tssl[0].BorderSides = ToolStripStatusLabelBorderSides.All;
        tssl[0].BorderStyle = Border3DStyle.SunkenInner;
        tssl[0].BackColor = SystemColors.Control;
        tssl[0].Text = "FormMDISkelton Ver. 1.0";
        tssl[0].AutoSize = true;
        tssl[0].TextAlign = ContentAlignment.MiddleLeft;
        tssl[1] = new ToolStripStatusLabel();
        tssl[1].BorderSides = ToolStripStatusLabelBorderSides.All;
        tssl[1].BorderStyle = Border3DStyle.SunkenInner;
        tssl[1].BackColor = SystemColors.Control;
        tssl[1].Text = "(メッセージ1)";
        tssl[1].AutoSize = true;
        tssl[1].TextAlign = ContentAlignment.MiddleLeft;
        tssl[2] = new ToolStripStatusLabel();
        tssl[2].BorderSides = ToolStripStatusLabelBorderSides.All;
        tssl[2].BorderStyle = Border3DStyle.SunkenInner;
        tssl[2].BackColor = SystemColors.Control;
        tssl[2].Text = "(メッセージ2)";
        tssl[2].ToolTipText = "(メッセージ2)";    //ToolTip設定
        tssl[2].Spring = true;
        tssl[2].TextAlign = ContentAlignment.MiddleLeft;
        statusStrip.Items.AddRange(tssl);
        statusStrip.ShowItemToolTips = true;        //ToolTip表示
        this.Controls.Add(this.statusStrip);        //StatusStrip(ステータスバー)を追加
        //メインフォームのレイアウトを再開
        this.ResumeLayout(false);
        this.PerformLayout();
    }

/*
//親フォームのMdiChildActivateイベントハンドラ
    private void ParentForm_MdiChildActivate(object sender, EventArgs e)
    {
        //アクティブな子フォームのタイトルを親フォームのタイトルに追加する
        if(this.ActiveMdiChild != null)
            this.Text = this.ActiveMdiChild.Text + " - " + Application.ProductName;
        else
            this.Text = Application.ProductName;
    }
*/

    //「新規」処理
    private void OnNew_Click(object sender, EventArgs e)
    {
        //子フォームとするフォームを作成する
        Form form = new Form();
        //サイズ決定
        form.Width = 506;
        form.Height = 277;
        //新規ファイルであることをウィンドウタイトル、ステータスバーとToolTipで表示
         form.Text = tssl[2].Text = tssl[2].ToolTipText = "新規ファイル";
        //親フォームをこのフォームにする
        form.MdiParent = this;
        //子フォームを表示する
        form.Show();
    }

    //「ファイルを開く」処理
    private void OnOpen_Click(object sender, EventArgs e)
    {
        DialogResult dr = MessageBox.Show("C#ファイルを開きますか?(「はい」)\r\nそれとも総てのファイルですか?(「いいえ」)\r\n或いは中止しますか?(「キャンセル」)", "ファイル種類確認", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
        OpenFileDialog ofDlg = new OpenFileDialog();
        ofDlg.AddExtension = true;    //拡張子自動付加
        switch(dr)
        {
        case DialogResult.Yes:
            ofDlg.FileName = "*.cs";    //拡張子
            ofDlg.FilterIndex = 1;        //ファイルフィルターインデックス
            break;
        case DialogResult.No:
            ofDlg.FileName = "*.*";        //拡張子
            ofDlg.FilterIndex = 2;        //ファイルフィルターインデックス
            break;
        default:
            return;
        }
        //ファイルフィルターの指定
        ofDlg.Filter = "C#ファイル(*.cs)|*.cs|総てのファイル(*.*)|*.*";
        ofDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
        ofDlg.CheckFileExists = true;    //ファイルの存在チェック
        ofDlg.CheckPathExists = true;    //ファイルパスの存在チェック
        ofDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
        ofDlg.Title = "ファイルを開く";    //ダイアログのタイトルを指定する
        if(ofDlg.ShowDialog() == DialogResult.OK)    //ダイアログを表示する
        {
        }
        else
        {
            MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        }
        // オブジェクトを破棄する
        ofDlg.Dispose();
        //子フォームとするフォームを作成する
        Form form = new Form();
        //サイズ決定
        form.Width = 506;
        form.Height = 277;
        //ウィンドウタイトル、ステータスバーとToolTipにファイル名を表示
         form.Text = tssl[2].Text = tssl[2].ToolTipText = ofDlg.FileName;
        //親フォームをこのフォームにする
        form.MdiParent = this;
        //子フォームを表示する
        form.Show();
    }

    //「保存」処理
    private void OnSave_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「名前を付けて保存」処理
    private void OnSaveAs_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「終了」処理
    private void OnExit_Click(object sender, EventArgs e)
    {
        Close();    //修了確認はOnClosingメソッドで行う
    }

    //「切り取り」処理
    private void OnCut_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「コピー」処理
    private void OnCopy_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「貼り付け」処理
    private void OnPaste_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「フォント」処理
    private void OnFont_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「検索」処理
    private void OnFind_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「置換」処理
    private void OnReplace_Click(object sender, EventArgs e)
    {
        MessageBox.Show("すみません、工事中です", "工事中", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    //「重ねて並べる」処理
    private void OnCascade_Click(object sender, EventArgs e)
    {
        //重ねて表示
        this.LayoutMdi(MdiLayout.Cascade);

//次のようにして現在アクティブな子フォームを特定して操作する
Form fm = this.ActiveMdiChild;
string str = String.Format("子フォームの幅 = {0}(クライアントエリア = {1})、高さ = {2}(クライアントエリア = {3})", fm.Width, fm.ClientSize.Width, fm.Height, fm.ClientSize.Height);
MessageBox.Show(str, "子ウィンドウサイズ", MessageBoxButtons.OK, MessageBoxIcon.Information);

    }
    //「縦に並べる」処理
    private void OnTileVert_Click(object sender, EventArgs e)
    {
        //上下に並べて表示
        this.LayoutMdi(MdiLayout.TileHorizontal);
    }

    //「横に並べる」処理
    private void OnTileHorz_Click(object sender, EventArgs e)
    {
        //左右に並べて表示
        this.LayoutMdi(MdiLayout.TileVertical);
    }

    //「アイコンの整列」処理
    private void OnArrIcon_Click(object sender, EventArgs e)
    {
        //アイコンの整列
        this.LayoutMdi(MdiLayout.ArrangeIcons);
    }

    //「使い方」処理
    private void OnHowtoUse_Click(object sender, EventArgs e)
    {
        MessageBox.Show("使い方処理です", "メニューアイテム", MessageBoxButtons.OK, MessageBoxIcon.Information);
    }

    //「バージョン」処理
    private void OnVersion_Click(object sender, EventArgs e)
    {
        VersionDlg verDlg = new VersionDlg(this.Icon);
        verDlg.ShowDialog();

        verDlg.Dispose();    //2023年06月30日追記

    }
}

/////////////////////
//Version ダイアログ
/////////////////////
class VersionDlg : Form
{
    public VersionDlg(Icon ico)
    {
        //ダイアログの属性設定
        this.Text = "バーション情報";
        this.ClientSize = new Size(320, 100);
        this.MaximizeBox = false;        // 最大化ボタン
        this.MinimizeBox = false;        // 最小化ボタン
        this.ShowInTaskbar = false;        //タスクバー上表示
        this.FormBorderStyle = FormBorderStyle.FixedDialog;        // 境界のスタイル
        this.StartPosition = FormStartPosition.CenterParent;    // 親フォームの中央に配置
        //コントロールの属性設定
        Button btnOK = new Button();
        btnOK.Size = new Size(40, 28);
        btnOK.Location = new Point(ClientSize.Width - btnOK.Width - 10, (ClientSize.Height - btnOK.Height) / 2);
        btnOK.Text = "OK";
        btnOK.Click += new EventHandler(OnOK_Click);
        Label imglabel = new Label();
        imglabel.Size = new Size(40, 40);
        imglabel.Location = new Point(10, (ClientSize.Height - imglabel.Height) / 2);
        imglabel.BorderStyle = BorderStyle.Fixed3D;
        imglabel.Image = ico.ToBitmap();    //親のシステムアイコン
        Label label = new Label();
        label.Size = new Size(ClientSize.Width - imglabel.Width - btnOK.Width - 40, ClientSize.Height- 20);
        label.Location = new Point(imglabel.Width + 20, (ClientSize.Height - label.Height) / 2);
        label.BorderStyle = BorderStyle.Fixed3D;
        label.Text = "C# MDI Skelton Version 1.0\r\nCopyright (c) 2023 by Ysama\r\n(written in Microsoft C#)";
        label.TextAlign = ContentAlignment.MiddleCenter;
        label.Font = new Font("Times New Roman", 10, FontStyle.Bold);
        this.Controls.Add(btnOK);
        this.Controls.Add(imglabel);
        this.Controls.Add(label);
    }

    private void OnOK_Click(object sender, EventArgs e)
    {
        Close();
    }
}

流石にMDIスケルトンだけで大したコード量ですが、C++で書くことに比べたら大分すっきりしますね。(欲を言うと定義と実装を分けてファイルにしたいんだけど。)

 

BCCSkeltonで作ったウィンドウ版「ハノイの塔」、とてもバランスよく適度なスピードで動き、気に入っています。

が、

一つだけ難点が。それは最初から書いていた(注)「ハノイの塔」の処理が長いので場合によってはタイムアウト(「応答なし」エラーで落ちる)してしまうことです。

注:【BCCSkelton】「ハノイの塔」プログラム(3)-設計仕様。特にハノイの塔を実行中に他のウィンドウ(プロセス)へフォーカスを移しちゃうと一発です。

 

ということで、「ハノイの塔」の処理部分だけマルチスレッドにして処理しようと考えましたが、背景用、マスク用、描画用の3つのビットマップの位置変数の書き込みでミューテックス、クリティカルセクション等を使っても、(特にMove関数部分で)描画位置や背景ビットマップ、描画ビットマップ、マスクビットマップの貼り付けで、↓の通りビミョーな差異が生じてしまいます。(背景ビットマップ、マスクビットマップ、描画ビットマップの座標がずれ、残像が残ってしまいます。)

 

 

この理由をいろいろと調べ、実験を行っていたら、WEBで次のような記述を見つけました。

 

Windows フォームは、本来アパートメントスレッドであるネイティブな Win32 ウィンドウに基づいているため、シングルスレッドアパートメント (STA) モデルを使用します。STA モデルでは、任意のスレッドでウィンドウを作成できるが、ウィンドウの作成後にスレッドを切り替えることはできない。したがって、ウィンドウに対するすべての関数呼び出しは、そのウィンドウを作成したスレッドで実行される必要があります。

 

これを調べていたらMicrosoft Learnでも「GDI は複数のスレッドをサポートしていません。 スレッドごとに個別のデバイス コンテキストと個別のレンダリング コンテキストを使用する必要があります。」という記述を見つけました。

 

この文章通りであれば、「ウィンドウを作って動かしているスレッド(UIスレッド)でしか描画できず、別スレッドにUIスレッドの描画を引き継ぐことは不可」ということになります。(デバイスコンテキストを別にするといっても、UIスレッドのウィンドウのコントロールを別スレッドで作る、ってダメでしょ?)

 

となると、「ハノイの塔の石盤移動描画」への対応は、

 

(1)別スレッドでビットマップの座標を計算し、それをキューに入れてUIスレッドで読みだして描画する。→ただし、こうしてもUIスレッドのウィンドウへの描画時間が長いので、結局タイムアウトする可能性がある。

(2)マルチスレッドを諦めてタイマー割込みで描画する。→ただし、基本的に「上へ移動、左右へ移動、下へ移動」という3つのループを使っているので、3つのフェーズのフラッグに合わせて呼び出す「3つの移動描画関数」に分けることが求められます。

(3)「あー、面倒くさいっ!」ということで、「Pop+Pushだけ」(コンソール版と同じ動作)とか、「Zオーダーの最前ウィンドウに固定して実行」する。(一応動くのですが、マウスを色々と弄ると矢張り落ちますね。また、当初の仕様とはならないので、これではイカサマですね。)

 

ということで、

 

ここでドツボにはまりましたぁ!

 

何らソリューションを見いだせないので、「他人はどうやっているのだろう?」ということでググってみると、

(1)C#ではApplication.DoEvents()というものがあり、これで「応答なし」エラーを回避できる、という反面、下手をするとスレッドが閉じられなくなる危険もあるとのこと。

(2)(実質C#と同じようなMicrosoftのC++/CLIではない)C++ではないのか、と調べるとBCBのApplication->ProcessMessage()関数があることはわかりましたが、これもVCLライブラリーの関数であることと、中で何をやっているのかわからないのでパス。

 

ただし、↑の(2)に関わる資料を読んでいて、どうも(1)と(2)は「処理の長いループの中に『ウィンドウのメッセージ処理』を入れてやり、メッセージキューが溢れないようにする」対処方法であることが分かりました。

 

ウィンドウのメッセージ処理は通常↓ののようになるのですが、

 

//ウィンドウのメッセージループ例

while((bRet = GetMessage( &msg, NULL, 0, 0 )) != 0) {

    if (bRet == -1) {

        //エラー処理

        break;

    }

    else {

        TranslateMessage(&msg);

        DispatchMessage(&msg);

    }

}

GetMessage関数は「メッセージキューからフィルターに適合メッセージがあるまで待機する」ので、こっちから積極的に読みに行くPeekMessage関数が、今回の場合は適当であると思われます。(注)

注:GetMessage関数とPeekMessage関数の違いー「PeekMessage 関数を使用すると、長い操作中にメッセージ キューを調べることができます。 PeekMessage は GetMessage 関数に似ています。両方とも、フィルター条件に一致するメッセージがないかメッセージ キューをチェックし、 メッセージを MSG 構造体にコピーします。 2 つの関数の主な違いは、フィルター条件に一致するメッセージがキューに配置されるまで GetMessage が返されないのに対し、 PeekMessage はメッセージがキュー内にあるかどうかに関係なく、すぐに返される点です。」(前出Microsoft Learn

 

基本設計として、TowerOfHanoi.hのユーザー定義関数に、

 

    //ユーザー定義関数
    bool DoEvent();                            //Moveの描画中にポストされたメッセージを処理する関数
 

を追加し、TowerOfHanoiProc.hの実装部分で、

 

bool CMyWnd::DoEvent() {
    MSG msg;
    
/*    第2引数がNULLの場合、PeekMessageは現在のスレッドに属する総てのウィンドウのメッセージと、
        HWND値がNULLである現在のスレッドのメッセージキュー上のすべてのメッセージを取得するので、
        ウィンドウとスレッドの両メッセージが処理される。(出典:Microsoft Learn)*/

    if(PeekMessage(&msg, (HWND)NULL, 0, 0, PM_REMOVE)) {
        
if(msg.message == WM_QUIT) {
            PostQuitMessage(0); 
   //キューから取り出してしまったので、もう一度メッセージキューに投函する
            return FALSE;        //その間に中断処理を行う
        }
        else {                    //それ以外のメッセージは処理する
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    return TRUE;
}


としてみました。赤字の部分はwhile文にしてポストされたメッセージを総て処理することもできますが、「上へ移動、左右へ移動、下へ移動」の3つのループに沿って一つづつ処理をしていくくらいのスピード感で大丈夫だろうと考え、「ポストされたメッセージを一つ読むだけ」のif文にしてみましたがバランス的には良いようです。

 

また、石盤移動描画処理中にその処理に影響のある割込みがなされると厄介なことになりますので、石盤段数を決定するコンボボックス(IDC_INIT)とハノイの塔を開始するボタン(IDC_START)は共に使用不可にし、「ハノイの塔」完了時に使用可に戻すようにします。

 

bool CMyWnd::OnStart() {
    //戻り値変数
    bool ret = TRUE;
    //段数選択状態の確認
    int num = SendItemMsg(IDC_INIT, CB_GETCURSEL, 0, (LPARAM)0);
    //未選択の場合のエラー処理
    if(num == CB_ERR) {
        MessageBox(m_hWnd, "段数が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        ret = FALSE;
    }
    else {

        //「ハノイの塔」関連コントロールを使用不可にする(「終了」だけ割込み可)
        EnableWindow(GetDlgItem(m_hWnd, IDC_INIT), FALSE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_START), FALSE);

        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, IDC_INIT), TRUE);

    }
    return ret;
}

 

この状態で考えられる割込みは「終了ボタン」またはシステムボタン(「X」)による終了なので、これに対処しないとなりません。↑のDoEvent関数では「WM_QUIT」メッセージの際(オレンジ文字部分)にキューから取り出したWM_QUITも再度ポストし、BOOLのFALSEを返すことで「WM_QUIT」を拾ったことを呼び出し側に伝えます。

呼び出し側のMove関数は、「上へ移動、左右へ移動、下へ移動」の3つのループで、

 

    while(y >= top) {                        //top迄上に動かす(例)
        m_Disk[disk].Erase();              //石盤を消す
        UpdateWindow(m_PicBox.m_hWnd);
        y--;                                        
//y座標を1ピクセル減らす
        m_Disk[disk].Show(x, y);         //石盤を表示する
        if(!DoEvent())        //WM_QUITを受け取ったら処理を中止する
            return FALSE;
    }

 

のようにし、更に呼び出し側のTowerofHanoi関数では、

 

//「ハノイの塔」メイン関数
void CMyWnd::TowerofHanoi(int diskCount, int fromPole, int toPole, int viaPole) {

    if(diskCount > 0) {
        TowerofHanoi(diskCount - 1, fromPole, viaPole, toP
 //ハノイの塔を描画する(石盤移動無し)
        //int num = Pop(fromPole);
        //Push(toPole, num);
        //MessageBox(m_hWnd, "In TowerofHanoi", "Check", MB_OK | MB_ICONINFORMATION);    //毎回の移動を確認する場合これを使ってください。
ole);
      
        
//ハノイの塔を描画する(石盤移動)
        if(!Move(fromPole, toPole))    //WM_QUITを受けた場合、
            return;                    //石盤移動描画処理を中止する
        TowerofHanoi(diskCount - 1, viaPole, toPole, fromPole);
    }

}

 

のように直ぐに処理を中断するようにします。(注)

注:WM_QUITメッセージの際の上記オレンジ部分の対応を取らずに単にDispatch関数処理を行うと、ウィンドウは消えますが、消えた後も(Move関数処理をしていて)プロセスが残ってしまいますので十分に注意してください。プロセスが完全に終了したか否かは、Ctrl+Alt+Delでタスクマネージャーを呼び出すと確認できます。(一応確実に終了することを私の環境で確認はしています。)

 

一応ここまでで当初の仕様通りの「ウィンドウ版ハノイの塔」が完成しました。(他のウィンドウの下に行っても、バックグラウンドで処理を続け、最小化しても元に戻せば処理を続けて、「応答なし」エラーが出ないようになりました。)更にSDIウィンドウ(CPICBOXではなく、CANVASで表示する)やDLL(コントロール化する)等、更に弄ることもできますが、一応これで「ハノイの塔」はお開きにしましょう。なお、「最終ウィンドウ版ハノイの塔」はBCCForm and BCCSkeltonパッケージのSampleBCCSkeltonに入れておきましたので、コードを詳しく見たい方はそちらを参照してください。

 

ps. C#、C++のコンソールから結構楽しめたことは確かですが、このマルチスレッド化失敗とソリューションの研究で大分エネルギーを使ったのも事実です。ちょっと休憩が必要ですね。

 

さてさて、正月ネタに「ハノイの塔」を取り上げ、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を使いましたが、矢張り手になじんでいるので簡単に作れますね。満足、満足。

 

前回までで基本的なウィンドウズ版の「ハノイの塔」のフレームワークができましたが、前々回でウィンドウズ版の「ハノイの塔」の一番重要な「表示」部分を簡単にスキップしていたことを忘れていました。

 

では、逆から攻め(全体から部品へ遡り)ましょう。

 

「ハノイの塔」コンピュータープログラムは、

    void TowerofHanoi(int(石盤数),

                                int(移動元のポール番号),

                                int(移動先のポール番号),

                                int(中継ポール番号)

         );

という関数を再帰的に使い、この関数の中で「移動元→移動先」の処理をしてやります。例えば、C++コンソール版の関数では↓の赤字の部分で「「石盤」クラスのインスタンスが嵌められる3本の「柱」クラスのインスタンス」を使い、「移動元(Pop)→移動先(Push)」処理を行い、その結果を表示しています。

    //「ハノイの塔」メイン関数
    void TowerofHanoi(int diskCount, int fromPole, int toPole, int viaPole) {

        if (diskCount > 0) {
            TowerofHanoi(diskCount - 1, fromPole, viaPole, toPole);
            m_pole[toPole].push(m_pole[fromPole].pop());
            ShowHanoi();    //(注)
            Sleep(100);
            //getch();    //毎回の移動を確認する場合これを使ってください。

            TowerofHanoi(diskCount - 1, viaPole, toPole, fromPole);
        }
注:コンソール版のShowHanoi関数は「柱」クラスのGetDisk関数で3本の柱の段の石盤の有無を上から調べ、文字列にして表示していました。

 

ウィンドウズ版の「ハノイの塔」はCMyClassというウィンドウを扱うクラス(ラッパー)ですが、その(ユーザー定義)メンバー関数に、前回やった通り、

    //ユーザー定義関数
    int Pop(int);
    void Push(int, int);

    void Move(int, int);    //移動状態も示すMoveですが、始まりはPop、終わりはPushと同じです。
がありました。コンソール版を異なるのはPopとPushの関数で石盤の消去と表示を行っている点です。(なのでのまだらにしています。)

 

ウィンドウズ版のPushとPop関数は、次のようになっています。コンソール版と同じく、は3本の柱間の9枚の石盤の移動データを処理し、表示関連処理です。部分は変えていないので同じですね。

///////////////////
//ユーザー定義関数
///////////////////

int CMyWnd::Pop(int fromPole) {    //引数は引き抜く柱(どこから)の番号

    int disk = m_Pole[fromPole].Pop();
    m_Disk[disk].Erase();    //石盤を消去する
    UpdateWindow(m_PicBox.m_hWnd);

    return disk;    //戻り値は石盤(何を)の番号
}

void CMyWnd::Push(int toPole, int disk) {    //引数は嵌める柱(どこへ)と石盤(何を)の番号

    int tire = m_Pole[toPole].Push(disk) + 1;    //ゼロベースか、1ベースかの違いです。
    m_Disk[disk].Show(m_Pole[pole].m_x - m_Disk[disk].m_w / 2, m_Pole[pole].m_y + DISKHEIGHT * (MAXTIRE - tire));    //引数が複雑ですが、基本的にビットマップを表示するx, y座標になっています。
    Sleep(DELAY);
}

 

3本の柱間の9枚の石盤の移動データ処理については、前にコンソール版で説明しましたが、柱毎に整数のスタック(FILO)があり、それが嵌められる石盤の番号をデータとして持っています。従って、特定の柱のPopは(石盤が嵌っていれば)一番上の石盤番号を返しますし、特定の柱のPushはスタックに嵌めた石盤の番号を入れます。

 

今回ウィンドウズ版で変更したのは表示関連で、具体的には「石盤」クラスのEraseShow関数です。これらについて前々回に示したコードで説明します。

///////////////////////////////////
//CDISKクラス-石盤の位置を記録し、
//表示、消去、移動を可能にする
///////////////////////////////////
class CDISK
{
private:
    HWND hm_hWnd;            //ビットマップを表示するウィンドウ
    CPICBOX* m_pPicBox = 0;    //CPICBOXオブジェクトポインター
    HDC m_hDC;                //ビットマップを表示するデバイスコンテキスト
    HBITMAP m_Disk = 0;        //石盤のビットマップ
    HBITMAP m_Mask = 0;        //石盤のマスクビットマップ
    HBITMAP m_Back = 0;        //石盤の背景ビットマップ

public:
    //メンバー変数
    int m_x = 0;            //石盤のx座標
    int m_y = 0;            //石盤のy座標
    int m_w = 0;            //石盤の幅
    int m_h = 0;            //石盤の高さ
//解説:これらはビットマップ表示用のメンバー変数として追加しました。

    //メンバー関数
    ~CDISK();                //デストラクター
    void Init(CPICBOX*, int, int);    //石盤(CDISKオブジェクト)の初期化
    void Show(int, int);    //石盤(CDISKオブジェクト)の表示
    void Erase();            //石盤(CDISKオブジェクト)の消去
//解説:これらがビットマップ表示に関して変更された関数です。

};

//デストラクター
CDISK::~CDISK() {
    //デバイスコンテキストの開放
    ReleaseDC(m_pPicBox->m_hWnd, m_hDC);    //解説:間違って取得したPICTUREBOXのHDCを開放
    //ビットマップの開放
    DeleteObject(m_Disk);
    DeleteObject(m_Mask);
    DeleteObject(m_Back);
//解説:デストラクターを設けたのはビットマップリソースとそれを表示するデバイスコンテキストの開放の為です。(実はここで、自分のやった大きな誤りに気が付きました。)

}

//石盤(CDISKオブジェクト)の初期化
void CDISK::Init(CPICBOX* pPB, int imgID, int maskID) {
    //表示するCPICBOXオブジェクトへのポインターを取得
    m_pPicBox = pPB;
    HINSTANCE hInst = (HINSTANCE)GetWindowLong(pPB->m_hWnd, GWL_HINSTANCE);
    //表示するデバイスコンテキストを取得
    m_hDC = GetDC(pPB->m_hWnd);    //解説:間違ってPICTUREBOXコントロールの現実のDCを取得している
    m_hDC = pPB->m_hDC;    //解説:仮想ウィンドウCPICBOXのm_hDCを使わないと不味であった。

//解説:次に9枚のビットマップリソースから特定の石盤の描画ビットマップとマスクビットマップを読み込みます。

    m_Disk = (HBITMAP)LoadImage(hInst, MAKEINTRESOURCEA(imgID), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADMAP3DCOLORS);
    m_Mask = (HBITMAP)LoadImage(hInst, MAKEINTRESOURCEA(
maskID), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADMAP3DCOLORS);
    BITMAP bmp;
    GetObject((HBITMAP)m_Disk, sizeof(BITMAP) , &bmp);
    m_w = bmp.bmWidth;
    m_h = bmp.bmHeight;

//解説:ビットマップの幅と高さを取得します。

}

//石盤(CDISKオブジェクト)の表示
void CDISK::Show(int x, int y) {
    //表示する背景をm_Backに取り込む(CPICBOXのGetBMP関数はデータ取得の度にデータが開放されるので使わない。)
    HDC memDC = CreateCompatibleDC(m_hDC);    //コンパチDCを作成
    m_Back = CreateCompatibleBitmap(m_hDC, m_w, m_h);    //コンパチビットマップを作成
    SelectObject(memDC, m_Back);    //ビットマップを選択
    StretchBlt(memDC, 0, 0, m_w, m_h, m_hDC, x, y, m_w, m_h, SRCCOPY);
    DeleteObject(memDC);    //コンパチDCを削除
//解説:石盤ビットマップを描画する前に描画エリアの背景をビットマップとして取得します。

    //マスクビットマップをANDで表示
    m_pPicBox->PutBMP(m_Mask, x, y, 1);
    //石盤のビットマップをORで表示
    m_pPicBox->PutBMP(m_Disk, x, y, 2);

//解説:CPICBOXラッパーを使うと簡単に描画できますね。

    //表示位置を記録
    m_x = x;
    m_y = y;

//解説:メンバー変数に新しい描画位置を記録(更新)します。

}

//石盤(CDISKオブジェクト)の消去
void CDISK::Erase() {
    //最新の背景ビットマップをm_Diskを表示した座標にPSETで貼り付ける
    m_pPicBox->PutBMP(m_Back, m_x, m_y, 0);

//解説:取得した背景ビットマップで上書きして石盤を消去します。

    //既存の背景ビットマップは削除し、初期化する
    DeleteObject(m_Back);
    m_Back = 0;

//解説:取得した背景ビットマップを開放、初期化します。

    m_x = 0;
    m_y = 0;

//解説:「石盤」を消去したので、描画位置を初期化します。

}

 

いかがでしょうか?「ハノイの塔」のデータ処理部分は変わらず、コンソールプログラムとウィンドゥズプログラムの違いは表示だけ、という点がお分かりになったでしょうか?

 

余談ですが、実は今回のウィンドウズ版「ハノイの塔」、Pop+Pushだとよいのですが、Moveだと表示でチラツキが大きく、タイミングを色々と変えても上手く表示されないので諦めかけていました。しかし、今回の解説で「本来CPICBOXの仮想ウィンドウに描画しなければならない所、CPICBOXの現実のウィンドウに描画しているという間違い」に気が付きました。(↑の赤字部分参照)

直ちにその間違いを修正(↑の修正部分参照)し、実行した所、石盤がスムースに移動するようになりました。ありがたや、ありがたや。(ぼけ老人にとって、ブログは良い見直しの機会ですね。)

 

前回でスケルトンが出来上がっているので、今回から「ハノイの塔」固有のコードを書いてゆきます。

まずは(Proc.hファイルに書く)処理の実装に先立ち、「何に何をさせるか」という設計仕様を(.hファイルに)書いてゆきます。以下ではTowerOfHanoi.hファイルの修正点を"//解説:"で説明してゆきます。

【設計仕様】

1.メインウィンドウはダイアログで、ハノイの塔の表示はPICTUREBOXコントロールを使用。

2.何段の石盤の塔にするのかはユーザー決定で、その入力は既定値をコンボボックスで選択する形にする。

3.ハノイの塔の開始とプログラムの終了はプッシュボタン。

4.ハノイの塔の計算はコンソールでも使った関数にし、ハノイの塔の石盤表示部分をビットマップを使う方法に変更。

5.石盤の表示は「背景ビットマップを取得→(マスクビットマップをAND表示→描画ビットマップをORで表示(表示))→背景ビットマップを表示(消去)とする。

6.石盤表示は最初「Push」「Pop」(結果だけ)で行い、次に「Move」(移動状態の表示)へ移行する。

7.「ハノイの塔」関数の実行時間は長く(Move関数を使うとさらに長く)、タイムアウトが生じると懸念されるのでマルチスレッドで実行する。STA、MTA()いずれの場合もハノイの塔の開始とプログラムの終了はプッシュボタンは開始後Disabledにしておく。

 

【TowerOfHanoi.h】

//////////////////////////////////////////
// TowerOfHanoi.h
// Copyright (c) 01/13/2023 by BCCSkelton
//////////////////////////////////////////
//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "BCCSkelton.h"
//リソースIDのヘッダー
#include    "ResTowerOfHanoi.h"
//ハノイの塔で使う石盤(CDISK)と柱(CPOLE)の定義ファイル
#include    "DiskandPole.h"
//解説:前回説明しましたハノイの塔の「柱」と「石盤」の定義ファイルをここで読み込みます。


/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCDLGクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CDLG
{
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CDLG(UName) {}
    //メンバー変数(表示関係)
    CPICBOX m_PicBox;        //表示用CPICBOXのインスタンス
    int m_Width = 0;        //m_PicBoxクライアントエリアの幅
    int m_Height = 0;        //m_PicBoxクライアントエリアの高さ
    HBITMAP m_BkGrnd = 0;    //背景ビットマップのハンドル
    //メンバー変数(「ハノイの塔」関係)
    CPOLE m_Pole[3];        //3本の柱
    int m_Tire = 0;            //選択された石盤の段数
    CDISK m_Disk[9];        //最大9つの石盤
//解説:これらはすべて本プログラム用に追加したメンバー変数です。表示関係ではPICTUREBOXをラップするCPICBOXクラスのインスタンス、クライアントエリアの幅、高さ、背景のビットマップハンドルがあります。また、ハノイの塔関係では「柱」三本、ユーザー設定の石盤の段数(最大9枚)、「石盤」9枚(最大)があります。

    //メニュー項目、ダイアログコントロール関連
    bool OnInit(WPARAM);
    bool OnStart();
    bool OnIdok();
//解説:それぞれ上から、石盤段数を指定するコンボボックス、ハノイの塔の開始ボタン、終了ボタンです。

    //ウィンドウメッセージ関連
    bool OnInit(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
    bool OnDestroy(WPARAM, LPARAM);
//解説:SkeltonWizardで選択したウィンドウメッセージです。

    //ユーザー定義関数
    int Pop(int);
    void Push(int, int);
    void Move(int, int);
    void TowerofHanoi(int, int, int, int);
//解説:当初、「柱」クラス、「石盤」クラスのインスタンスと連動したPush(石盤を柱にはめる)、Pop(柱から石盤を取り出す)と主役の「ハノイの塔」関数だけでしたが、その後石盤の移動を表示させるMove関数を追加しました。(これについてはまた書きます。)
};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////
CMyWnd TowerOfHanoi("TowerOfHanoi");    //ウィンドウクラスインスタンスの生成

BEGIN_MODELESSDLGMSG(ModelessProc, TowerOfHanoi)    //コールバック関数名は主ウィンドウの場合ModelessProcにしている
    //メニュー項目、ダイアログコントロール関連
    ON_COMMAND(TowerOfHanoi, IDC_INIT, OnInit(wParam))
    ON_COMMAND(TowerOfHanoi, IDC_START, OnStart())
    ON_COMMAND(TowerOfHanoi, IDOK, OnIdok())
    //ウィンドウメッセージ関連
    //自動的にダイアログ作成時にOnInit()、終了時にOnClose()を呼びます
    //ON_CLOSE(TowerOfHanoi)

    ON_DESTROY(TowerOfHanoi)
//解説:前にも告白しましたが、SkeltonWizardでダイアログベースの定番のWM_INITDIALOG、WM_CLOSE、WM_DESTROYを選択すると、(すでに「自動的にダイアログ作成時にOnInit()、終了時にOnClose()を呼びます」)なので「ON_DESTROY(TowerOfHanoi)」だけになるべきところ、「ON_CLOSE(TowerOfHanoi)」が書き出されてこの処理が二重になる問題があるので、書かれたものは消します。

END_DLGMSG

 

これは前にBraphMakerで詳しく解説したので、改めては説明しませんが、今回ダイアログを作る前にPICTUREBOXコントロールを登録する必要があります。

【TowerOfHanoi.cpp】

//////////////////////////////////////////
// TowerOfHanoi.cpp
//Copyright (c) 01/13/2023 by BCCSkelton
//////////////////////////////////////////
#include    "TowerOfHanoi.h"
#include    "TowerOfHanoiProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!TowerOfHanoi.IsOnlyOne()) {
        HWND hWnd = FindWindow("MainWnd", "TowerOfHanoi");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }
    //CPICBOX(ウィンドウ)クラスを登録する
    if(!TowerOfHanoi.m_PicBox.Register(hInstance))
        MessageBoxW(NULL, L"PictureBoxが登録できませんでした", L"警告", MB_OK | MB_ICONSTOP);


    //モードレスダイアログを作成Create(hParent, DlgName, DlgProc);
    if(!TowerOfHanoi.Create(NULL, hInstance, "IDD_MAIN", ModelessProc))
        return 0L;

    //メッセージループに入る
    return TowerOfHanoi.Loop();
}
 

このように登録すると、前回"STATIC"コントロールにしていたIDC_SCREENを"PICTUREBOX"に指定してもエラーにならなくなります。

 

このリソースファイル(TowerOfHanoi.rc)をお約束のSkeltonWizardにかけて、ダイアログ(IDD_MAIN)とアイコン(IDI_ICON)を指定し、ウィンドウメッセージからWM_INITDIALOG、WM_CLOSE、WM_DESTROYを選択し、コントロールはIDC_INIT、IDC_START、IDOKを選択して完了します。(注)

注:前のGraphMakerの解説を参考にしてください。

 

ここまでで出来上がったファイルは次の通りです。

TowerOfHanoi.rc

ResTowerOfHanoi.h

TowerOfHanoi.bdp

TowerOfHanoi.h

TowerOfHanoiProc.h

TowerOfHanoi.cpp

 

この後、ウィンドウ版「ハノイの塔」用にやや修正した「柱」クラスと「石盤」クラスの定義ファイルを追加してやります。内容は以下の通りです。(コンソール版の際は、先行開発したC#の色を残しましたが、今回はC++として書いています。)

【DiskandPole.h】

/////////////////////////
//「ハノイの塔」定数定義
/////////////////////////
#define        DISKWIDTH    160        //最大石盤ビットマップの幅(pixel)
#define        DISKHEIGHT    32        //石盤ビットマップの厚さ(pixel)
#define        MAXTIRE        9        //最大段数

///////////////////////////////////
//CDISKクラス-石盤の位置を記録し、
//表示、消去、移動を可能にする
///////////////////////////////////
class CDISK
{
private:
    HWND hm_hWnd;            //ビットマップを表示するウィンドウ
    CPICBOX* m_pPicBox = 0;    //CPICBOXオブジェクトポインター
    HDC m_hDC;                //ビットマップを表示するデバイスコンテキスト
    HBITMAP m_Disk = 0;        //石盤のビットマップ
    HBITMAP m_Mask = 0;        //石盤のマスクビットマップ
    HBITMAP m_Back = 0;        //石盤の背景ビットマップ
public:
    //メンバー変数
    int m_x = 0;            //石盤のx座標
    int m_y = 0;            //石盤のy座標
    int m_w = 0;            //石盤の幅
    int m_h = 0;            //石盤の高さ
    //メンバー関数
    ~CDISK();                //デストラクター
    void Init(CPICBOX*, int, int);    //石盤(CDISKオブジェクト)の初期化
    void Show(int, int);    //石盤(CDISKオブジェクト)の表示
    void Erase();            //石盤(CDISKオブジェクト)の消去
};

//デストラクター
CDISK::~CDISK() {
    //デバイスコンテキストの開放
    ReleaseDC(m_pPicBox->m_hWnd, m_hDC);
    //ビットマップの開放
    DeleteObject(m_Disk);
    DeleteObject(m_Mask);
    DeleteObject(m_Back);
}

//石盤(CDISKオブジェクト)の初期化
void CDISK::Init(CPICBOX* pPB, int imgID, int maskID) {
    //表示するCPICBOXオブジェクトへのポインターを取得
    m_pPicBox = pPB;
    HINSTANCE hInst = (HINSTANCE)GetWindowLong(pPB->m_hWnd, GWL_HINSTANCE);
    //表示するデバイスコンテキストを取得
    m_hDC = GetDC(pPB->m_hWnd);
    m_Disk = (HBITMAP)LoadImage(hInst, MAKEINTRESOURCEA(imgID), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADMAP3DCOLORS);
    m_Mask = (HBITMAP)LoadImage(hInst, MAKEINTRESOURCEA(maskID), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_LOADMAP3DCOLORS);
    BITMAP bmp;
    GetObject((HBITMAP)m_Disk, sizeof(BITMAP) , &bmp);
    m_w = bmp.bmWidth;
    m_h = bmp.bmHeight;
}

//石盤(CDISKオブジェクト)の表示
void CDISK::Show(int x, int y) {
    //表示する背景をm_Backに取り込む
    HDC memDC = CreateCompatibleDC(m_hDC);                //コンパチDCを作成
    m_Back = CreateCompatibleBitmap(m_hDC, m_w, m_h);    //コンパチビットマップを作成
    SelectObject(memDC, m_Back);                        //ビットマップを選択
    StretchBlt(memDC, 0, 0, m_w, m_h, m_hDC, x, y, m_w, m_h, SRCCOPY);
    DeleteObject(memDC);                                //コンパチDCを削除
    //マスクビットマップをANDで表示
    m_pPicBox->PutBMP(m_Mask, x, y, 1);
    //石盤のビットマップをORで表示
    m_pPicBox->PutBMP(m_Disk, x, y, 2);
    //表示位置を記録
    m_x = x;
    m_y = y;
}

//石盤(CDISKオブジェクト)の消去
void CDISK::Erase() {
    //最新の背景ビットマップをm_Diskを表示した座標にPSETで貼り付ける
    m_pPicBox->PutBMP(m_Back, m_x, m_y, 0);
    //既存の背景ビットマップは削除し、初期化する
    DeleteObject(m_Back);
    m_Back = 0;
    m_x = 0;
    m_y = 0;
}

//////////////////////////////
//CPOLEクラス-石盤を嵌めたり、
//抜いたりする柱を表現する
//////////////////////////////
class CPOLE
{
private:
    //メンバー変数
    int* m_Disks;        //石盤配列ポインター
    int m_Tire = 0;        //石盤段数ポインター
public:
    int m_x = 0;        //描画用x座標
    int m_y = 0;        //描画用y座標
    //メンバー関数
    CPOLE();            //コンストラクター
    ~CPOLE();            //デストラクター
    void Init();        //配列の初期化
    int GetNext();        //次に置く段を求める
    int Push(int);        //柱に盤を嵌める
    int Pop();            //柱から盤を抜き、盤数を返す(なければ0)
    int GetDisk(int);    //柱のnum番目の盤数を返す(なければ0)
};

//コンストラクター
CPOLE::CPOLE() {
    m_Disks = new int [MAXTIRE];
}

//デストラクター
CPOLE::~CPOLE() {
    delete [] m_Disks;
}

//配列の初期化
void CPOLE::Init() {
    for(int i = 0; i < MAXTIRE; i++)
        m_Disks[i] = 0;
        m_Tire = 0;
}

//次に置く段を求める
int CPOLE::GetNext() {
    return m_Tire;
}

//柱に盤を嵌める
int CPOLE::Push(int num) {
    if(m_Tire == MAXTIRE)
        return -1;
    m_Disks[m_Tire] = num;
    return m_Tire++;
}

//柱から盤を抜き、盤数を返す(なければ0)
int CPOLE::Pop() {
    int val = 0;
    if(m_Tire > 0)
    {
        val = m_Disks[m_Tire - 1];
        m_Disks[m_Tire - 1] = 0;
        --m_Tire;
    }
    return val;
}

//柱のnum番目の盤数を返す(なければ0)
int CPOLE::GetDisk(int num) {
    return m_Disks[num];
}
//解説:GetDisk関数はコンソール版用なので不要でしたが、削除していません。

 

繰り返しになりますが、BCCSkelton(ECCSkelton)によるプログラミングは、まず視覚的な完成形をイメージして、リソースを書くことから始まります。(注)

注:「6)発想は「積み上げ型」ではありません。先ず完成形のイメージをもち、BCCFormでリソース部品を作り、そのRCファイルからスケルトン作成ウィザードで基本的なウィンドウの型を作って、後はメニューやコントロールに対応した関数にその部分のコーディングをしていくだけです。」(「SkeltonWizard BCCSkeltonライブラリー」ヘルプファイル、「BCCSkeltonライブラリーとは何か?」から引用)

 

1.完成形のイメージ

今回のハノイの塔の場合、ベトナム(Vietnum-ベトナムではないですが、ヴィェツゥナム、とも一寸違いますね)のハノイには数回出張で行きましたが、あまり仏教由来の印象的建築物はなく、イメージが違いますね。実際、この数学パズルの由来となる伝説ではインドのガンジス川河畔の寺院にあるダイアモンド針と青銅盤なのだそうです。しかし、これも絵的には回避したいですね。

 

ということで、行き着いた背景は「アンコールワット(同じIndchineでもこれはカンボジアですね。でも私にとってのIndchineはココでやはり本店はベトナムのです)」、その寺院を背景に「木柱が3本」立ち、それに「花崗岩の石盤」が嵌っている映像が浮かび上がりました。

 

2.リソース画像の調達

ということで、フリーの画像を探しにインターネットへ。500pixel x 500pixelのフリーの背景画(画中の"M/Y/D/G"は消さないでください。これがフリーの条件となっています)と花崗岩表面の写真を入手します。

 

3.石盤を作る

完成している背景はさておき、ここからの石盤つくりが大変です。

 

まず、ウィンドウ上のイメージ(画像)の表示はBitmap(他の形式でも最終的にはビットマップに変換されるようです)によりますが、そのサイズは表示できる制限内にするために石盤個数も制限がかかります。今回の場合、背景が500pixelなので、柱3本における最大の石盤は2の乗数でもある16の倍数で160 x 3 = 480(残りの20は間隔マージン)としました。一番小さい石盤を16 x 16とすると正方形になるので、最小は32 x 16とし、最大9枚(幅 = 16 + 16 x 1~9枚)にしました。(間隔マージンは左右と柱間の4か所で5となります。)→ということで"#define MAXTIRE 9"の定数定義を思い浮かべますね。

 

次にビットマップは9つの石盤のビットマップを作ればよいわけではなく、描画して次に消去するために「描画部分の背景を切り取ったビットマップ(注)」が必要です。また、石盤を長方形にするととても機械的で面白くないので、角を丸くしてやる必要がありますが、その場合その丸みをつけた部分に背景を残さなければならない為、アイコンのように「描画ビットマップ」と「マスクビットマップ」が必要になります。

注:このビットマップはプログラムの描画位置に応じて動的に生成します。

 

例えば↓の例ではキャラクターの「描画ビットマップ」と描画部分を黒(0)背景部分を白(1)にした「マスクビットマップ」を用意し、背景にマスクビットマップをANDすることで描画部分が黒(0)抜きになり、背景部分は白(1)とのANDなので元のままとなります。次に「描画ビットマップ」をORすることで、描画部分が描かれ、背景部分は黒(0)とのORでそのまま残ります。

 

石盤のビットマップは、まず長方形の9枚のベース画像を作り、それをMS Paintの「角丸四角形」を黒色で上囲み(下はそのまま)します。また、このベースを黒塗りし、今度は同様に「角丸四角形」を白色で上囲みします。

これを9枚分作ることになります。

 

アイコンはコンソール版を作るときに作った↓を流用します。

 

4.ダイアログを作る

前回のMakeGraphで使ったばっかりだったので、今回もCPICBOXクラスを使い、サイズ不変更のダイアログにしました。

石盤の段数入力はコンボボックスを使い、選択時(CB_SELCHANGEメッセージが出ます)に指定段数(m_Tire <= MAXTIREになります)での初期化(石盤を左の第0番柱にPushしてゆきます)を行います。

「ハノイの塔」関数の実行は「開始」ボタンを押します。

プログラムの終了はいつも通り、IDOKボタンを押します。

PICTUREBOXコントロールはまだ登録されていないので"STATIC"で作ってください。後で手作業で"PICTUREBOX"に変えます。(↓は完成形なので"PICTUREBOX"になっていますが、最初はビットマップ貼り付け用"STATIC"にしていました。)

 

なお、これを作っていて、再帰を使ったプログラムなので実行時間が長くなる可能性があり、シングルスレッドのウィンドウズプログラムの場合、タイムアウトで落ちることが懸念されましたが、最初は落ちてもよいのでシングルスレッドのままとします。

 

【TowerOfHanoi.rc】

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResTowerOfHanoi.h"

//----------------------------------
// ダイアログ (IDD_MAIN)
//----------------------------------
IDD_MAIN DIALOG DISCARDABLE 0, 0, 426, 383
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT | DS_MODALFRAME | DS_CENTER
CAPTION "TowerOfHanoi"
FONT 8, "MS 明朝"
{
 CONTROL "", IDC_INIT, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 356, 27, 60, 96, WS_EX_CLIENTEDGE
 CONTROL "開始", IDC_START, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 356, 48, 60, 19
 CONTROL "終了", IDOK, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 356, 352, 60, 19
 CONTROL "石盤の段数", IDC_LABEL, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY | SS_CENTER, 357, 11, 58, 12
 CONTROL "", IDC_SCREEN, "PICTUREBOX", WS_CHILD | WS_VISIBLE, 9, 9, 336, 366, WS_EX_CLIENTEDGE
}

//--------------------------
// イメージ(IDI_ICON)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "TowerOfHanoi.ico"

//--------------------------
// イメージ(IDI_BACKGRND)
//--------------------------
IDI_BACKGRND    BITMAP    DISCARDABLE    "アンコールワットフリー絵500x500.bmp"

//--------------------------
// イメージ(IDI_DISK1)
//--------------------------
IDI_DISK1    BITMAP    DISCARDABLE    "石盤32x32.bmp"

//--------------------------
// イメージ(IDI_DISK2)
//--------------------------
IDI_DISK2    BITMAP    DISCARDABLE    "石盤48x32.bmp"

//--------------------------
// イメージ(IDI_DISK3)
//--------------------------
IDI_DISK3    BITMAP    DISCARDABLE    "石盤64x32.bmp"

//--------------------------
// イメージ(IDI_DISK4)
//--------------------------
IDI_DISK4    BITMAP    DISCARDABLE    "石盤80x32.bmp"

//--------------------------
// イメージ(IDI_DISK5)
//--------------------------
IDI_DISK5    BITMAP    DISCARDABLE    "石盤96x32.bmp"

//--------------------------
// イメージ(IDI_DISK6)
//--------------------------
IDI_DISK6    BITMAP    DISCARDABLE    "石盤112x32.bmp"

//--------------------------
// イメージ(IDI_DISK7)
//--------------------------
IDI_DISK7    BITMAP    DISCARDABLE    "石盤128x32.bmp"

//--------------------------
// イメージ(IDI_DISK8)
//--------------------------
IDI_DISK8    BITMAP    DISCARDABLE    "石盤144x32.bmp"

//--------------------------
// イメージ(IDI_DISK9)
//--------------------------
IDI_DISK9    BITMAP    DISCARDABLE    "石盤160x32.bmp"

//--------------------------
// イメージ(IDI_MASK1)
//--------------------------
IDI_MASK1    BITMAP    DISCARDABLE    "Mask32x32.bmp"

//--------------------------
// イメージ(IDI_MASK2)
//--------------------------
IDI_MASK2    BITMAP    DISCARDABLE    "Mask48x32.bmp"

//--------------------------
// イメージ(IDI_MASK3)
//--------------------------
IDI_MASK3    BITMAP    DISCARDABLE    "Mask64x32.bmp"

//--------------------------
// イメージ(IDI_MASK4)
//--------------------------
IDI_MASK4    BITMAP    DISCARDABLE    "Mask80x32.bmp"

//--------------------------
// イメージ(IDI_MASK5)
//--------------------------
IDI_MASK5    BITMAP    DISCARDABLE    "Mask96x32.bmp"

//--------------------------
// イメージ(IDI_MASK6)
//--------------------------
IDI_MASK6    BITMAP    DISCARDABLE    "Mask112x32.bmp"

//--------------------------
// イメージ(IDI_MASK7)
//--------------------------
IDI_MASK7    BITMAP    DISCARDABLE    "Mask128x32.bmp"

//--------------------------
// イメージ(IDI_MASK8)
//--------------------------
IDI_MASK8    BITMAP    DISCARDABLE    "Mask144x32.bmp"

//--------------------------
// イメージ(IDI_MASK9)
//--------------------------
IDI_MASK9    BITMAP    DISCARDABLE    "Mask160x32.bmp"
 

ps. これだけビットマップを内蔵して使っているのでプログラムサイズも半端ない(979KB)です。仕方無いですよね。

 

娘と孫が来ていたり、義父を施設に入れるためにドタバタしていましたが、偉そうに

前回、大分吹かしましたが、結局ウィンドウプログラムは石盤のビットマップを作らなければならないし、数を多くすると非常に疲れそうなので退けて(尻込みして)しまいました。(また、長い時間をかけて作っても、1、2回見たらすぐに飽きると思うので、むなしい努力かなぁ、とテンションが下がったのも事実です。)

等と書いたものの、基本暇なのでとうとうC#コンソール、C++コンソールのプログラムに続き、前に書いた通りBCCSkeltonでウィンドウプログラムを組んでみました。

 

といっても、コンソールプログラムと「ハノイの塔」関数は同じなので、

(1)ウィンドウをどうするか、「ウィンドウ上のハノイの塔の表示をどうするか」の違いしかない。

(2)その際に「背景」、「柱」、「石盤」のリソースをどうするか決め、

(3)データの「柱」、「石盤」と表示リソースの「柱」、「石盤」をリンクさせてPush(柱に表示)、Pop(柱から消去)させなければならない。

(4)その際に、イメージ(今回はビットマップ)をどのようにして背景に貼るのか、消去したときに背景をどうやって復活させるのか、を決めなければならない、ということです。

 

「ハノイの塔」のアルゴリズム等はコンソールプログラムでやったので、これからはBCCForm and BCCSkeltonで「ハノイの塔」のウィンドウプログラムを開発する際の苦心譚に絞って書こうと思います。

 

尚、「ハノイの塔」のウィンドウプログラムはBCCForm and BCCSkeltonのサンプルに入れてアップロード予定ですが、のちに書く改善点があるので、将来は改良されたプログラムになるかもしれません(し、面倒くさいのでやらないかもしれません)のでご了解ください。

 

【本日段階のウィンドウ版「ハノイの塔」】

 

前回C#でコンソールプログラムを書いた後、まだ次のお題が見つかっていなかったので、暇に任せて(C#のプログラムをベースとして)C++でコンソールプログラムを書いてみました。

 

結果論でいうと「文字列操作とコンソールへの文字列表示」の部分を除くとほとんどそのままC#から移植できました。一方、C#ではUnicodeベースのstring型文字列変数が機能が豊富で使いやすく簡単でしたが、C++では2バイト文字のANSI文字列を操作するのは結構面倒だったのと、(コンソールプログラムでユニコードを扱うのは今回が初めてでしたが)C++ライブラリーでユニコード文字列を扱うのは想像以上に面倒であり、書かれている通りにロケールを設定(注1)してWCHARとwcout、wcin(注2)を利用しても正常に動作しなかった為、ユニコードプログラムは断念しました。

注1:#include <locale.h>とsetlocale関数で、setlocale(LC_CTYPE, <引数>);の引数に"Japanese"や"JP-jp"(これは"jp-JP"が正しかったようですが)などを入れても反応せず、やっと"Japanese_Japan.932"でやっと文字が表示されました。

注2:最初どのヘッダーファイルを使うのか、cinとcoutはそのまま使えるのかわからなかったのですが、ウェブで調べて#include <iostream>でwcinとwcoutを使えることが分かりました。(データはWCHARとL"..."です。)しかし、注1の対応でwcoutは動いたのですが、wcinが正常に動きませんでした。

 

この為、あえてC++ではstringを使わずにANSIのcharとchar*を使い、Shift-JISの全角文字も使わずにASCII文字で塔を(半角文字の'O'、'|'と'-'で)描画しています。(何故かプログラミングをしていて郷愁を感じましたよ。)

いかがでしょうか?

C#は中間言語を使ったWindows依拠の実行環境ですが、速度的にはC++と差はないように感じます。しかし、一番印象的であったのはプログラムサイズで、Embarcadero C++コンパイラーはランタイムを使うのでDLL込みで194KBもありますが、MicroSoftのC#コンパイラーはDLLを使っていてもそのサイズは入らないのでたったの10KBだけでした。

 

ご参考になれば幸いです。

 

【ANSI C++によるハノイの塔】

//////////////////////////////////////////////////////////////////
// TowerOfHanoi.cpp
// Tower of Hanoi, a mathimatic game
// https://www.programmingalgorithms.com/algorithm/tower-of-hanoi/
//////////////////////////////////////////////////////////////////

#define        MAXTIRE        10        //最大段数(これより多くする場合、DISK定数も変更する)
#define        DISK        "OOOOOOOOOOOOOOOOOOOO"
#define        HORIZEN        "------------------------------------------------------------"

#include    <windows.h>            //Sleep関数使用の為
#include    <stdio.h>
#include    <conio.h>            //getch()使用の為
#include    <stdlib>
#include    <iostream>

using namespace std;

/////////////////////////////
//Poleクラス-石盤を嵌めたり、
//抜いたりする柱を表現する
/////////////////////////////
class Pole
{
    //メンバー変数
    int* m_Disks;
    int m_Tire = 0;

public:
    //コンストラクター
    Pole() {
        m_Disks = new int [MAXTIRE];
    }

    //コンストラクター(引数付)
    Pole(int num)
    {
        if(num > MAXTIRE) {
            m_Disks = new int [MAXTIRE];
        }
        else {
            m_Disks = new int [num];
        }
    }

    //デストラクター
    ~Pole() {
        delete [] m_Disks;
    }

    //柱に盤を嵌める
    void push(int num) {

        m_Disks[m_Tire] = num;
        m_Tire++;
    }

    //柱から盤を抜き、盤数を返す(なければ0)
    int pop() {

        int val = 0;
        if(m_Tire > 0)
        {
            val = m_Disks[m_Tire - 1];
            m_Disks[m_Tire - 1] = 0;
            --m_Tire;
        }
        return val;
    }

    //柱のnum番目の盤数を返す(なければ0)
    int getDisk(int num) {

        return m_Disks[num];
    }
};

/////////////////////
//TowerOfHanoiクラス
/////////////////////
class TowerOfHanoi
{
    //メンバー変数
    Pole m_pole[3];        //3本の柱
    int m_tire = 0;        //柱の段数

public:
    //コンストラクター
    TowerOfHanoi(int num) {
        //最初の柱に石盤を設置する
        m_tire = num;
        for(int i = 0; i < m_tire; i++)
            m_pole[0].push(m_tire - i);
    }
    //「ハノイの塔」メイン関数
    void TowerofHanoi(int diskCount, int fromPole, int toPole, int viaPole) {

        if (diskCount > 0) {
            TowerofHanoi(diskCount - 1, fromPole, viaPole, toPole);
            m_pole[toPole].push(m_pole[fromPole].pop());
            ShowHanoi();
            Sleep(100);
            //getch();    //毎回の移動を確認する場合これを使ってください。
            TowerofHanoi(diskCount - 1, viaPole, toPole, fromPole);
        }
    }

    //「ハノイの塔」表示関数
    void ShowHanoi() {

//        system("cls");                //画面クリア(そのほかにもsystem("pause")→「続行するには何かキーを押してください...」等がある)
        cout << "\x1B[2J\x1B[H";    //エスケープシーケンスによる画面クリアー
        cout << ">>>>> Tower of Hanoi <<<<<\n" <<endl;
        for(int i = m_tire - 1; i >= 0; i--)
            cout << MakeDisk(m_pole[0].getDisk(i)) << MakeDisk(m_pole[1].getDisk(i)) << MakeDisk(m_pole[2].getDisk(i)) << endl;
        //最後に「地面」線を描く
        cout << HORIZEN << endl;
    }

    //「ハノイの塔石盤作成関数」
    char* MakeDisk(int num)
    {
        if(num > MAXTIRE) {    //MAXTIREを超えた場合のエラー処理
            MessageBox(0, "不正な石盤番号です", "エラー", MB_OK | MB_ICONERROR);
            return NULL;
        }
        char static ln[MAXTIRE * 2 + 1] = {0};        //(MAXTIREX x 2)文字とnull終端分の行を作成
        for(int i = 0; i < (MAXTIRE * 2); i++)        //(MAXTIREX x 2)文字分を空白文字で初期化(最終はnullのまま)
            ln[i] = ' ';
        if(!num)
            ln[10] = ln[9] = '|';                    //柱
        else {                                        //石盤
            memcpy(ln + (MAXTIRE - num), DISK, num * 2);
        }
        return ln;
    }
};

/////////////
// Main関数
/////////////
int main() {

    //段数のユーザー設定
    int tire = 0;
    while(tire <= 0) {
        cout << "石盤柱の段数(整数)を入力してください(小数は切り捨てられます):";
        cin >> tire;
    }
    //ハノイの塔のインスタンスを作成
    TowerOfHanoi TOH(tire);
    //「ハノイの塔」の初期状態を表示
    TOH.ShowHanoi();
    cout << "開始します。何かキーを入押してください:" << endl;
    getch();
    //「ハノイの塔」の開始
    TOH.TowerofHanoi(tire, 0, 1, 2);
    //コンソールが閉じるのを停止させる
    cout << "終了しました。何かキーを入押してください:" << endl;
    getch();
    return 0L;
}