MENU

【C#】ペントミノ2Dパズルを解く(8)フォームのコード

やっと Form1 のコードです。C#のフォームはVB.Netのフォームとほぼ一緒ですが、イベントの記述は若干の違いがあるため「慣れ」が必要です。

・クラス宣言

IDEのソリューションエクスプローラで Form1 を右クリックして "コードの表示" で自動表示されるので気にも留めませんが、クラスの名前は "Form1" です。

public partial class Form1 : Form

・State プロパティ (状態遷移)

コントロールの状態はプロパティ(State)で管理します。値を変更すると個々のコントロールの Enabled や Visible プロパティを一括して変更できるようにしています。

状態 入力関係のコントロール プログレスバーとキャンセルボタン
フォーム入力時 1 Enabled = True Visible = False
探索作業中 2 Enabled = False Visible = True
private int _State = 0;

private int State
{
    get
    {
        return _State;  
    }
    set
    {
        _State = value;
        bool[] STS = { value == 0, value == 1, value == 2 };
        groupBox1.Enabled = STS[1];
        cbx_都度確認あり.Enabled = STS[1];
        探索開始ボタン.Enabled = STS[1];
        progressBar1.Visible = STS[2];
        キャンセルボタン.Visible = STS[2];
    }
}

//===== 例 =====
State = 1;     //入力関係のコントロールは活性化、プログレスバーとキャンセルボタンは非表示にします

・フォームを開くとき

各コントロールの初期値をセットして State プロパティを 1 にします。

public Form1()
{
    InitializeComponent();
    rbt_10x6.Checked = true;
    State = 1;
}

・フォームを閉じるとき

フォームのイベントは FormClosing です。State プロパティが 2 のときは、探索作業中にキャンセルボタンを押したりしてここに来たことが考えられるので「中止しますか?」とユーザーに確認します。State プロパティが 1 のときはそのままクローズします。

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if(State == 2)
    {
        DialogResult result = MessageBox.Show("中止しますか?","", MessageBoxButtons.YesNo);
        if(result == DialogResult.Yes)
        {
            DEF.cancelFlag = true;
        }
        e.Cancel = true;
    }
}

・キャンセルボタンを押したとき

フォームのイベントは Click です。キャンセルボタンを押したときにいきなり「中止しますか?」と確認してはいけません。フォーム右上の[×]やタスクバーで閉じる場合もあるので、キャンセルボタンではフォームを閉じるイベントを発生させて、他の閉じる機能と同じように「フォームを閉じるとき」に管理を任せるべきです。

private void キャンセルボタン_Click(object sender, EventArgs e)
{
    Close();
}

・探索開始ボタンを押したとき

ここからがパズルを解く部分です。

private void 探索開始ボタン_Click(object sender, EventArgs e)
{
    // (1) State変更と DEF.CancelFlag の準備
    State = 2;
    DEF.cancelFlag = false;

    // (2) ボードの用意
    Board board;
    if (rbt_10x6.Checked)
    {
        board = new Board(10, 6);
    }
    else if (rbt_12x5.Checked)
    {
        board = new Board(12, 5);
    }
    else if (rbt_15x4.Checked)
    {
        board = new Board(15, 4);
    }
    else
    {
        board = new Board(20, 3);
    }
    リストビュー初期化(board);

    // (3) 部品棚を作成
    List<Shelf> shelves = new List<Shelf>();
    DEF.部品棚作成(shelves);

    // (4) プログレスバーを初期化
    progressBar1.Maximum = shelves.Sum(s => s.Pieces.Count);
    progressBar1.Minimum = 0;
    progressBar1.Value = 0;

    // (6) 探索実行
    DateTime startTime = DateTime.Now;
    DEF.solutionNo = 0;
    nextSearch(board, shelves, new Coord(0, 0));
    DateTime endTime = DateTime.Now;

    // (7) 結果表示と終了
    if (!DEF.cancelFlag)
    {
        progressBar1.Value = progressBar1.Maximum;
        MessageBox.Show("探索終了\r\n\r\n" + DEF.経過時間(startTime, endTime),DEF.myName);
    }
    State = 1;
    Close();
}
(1) State変更と DEF.CancelFlag の準備

State を 2 に変更してコントロールのプロパティを「探索作業中」モードに切り替えます。同時に DEF.cancelFlag に初期値を与えて準備します。

(2) ボードの用意

ラジオボタンの選択にしたがってボードのサイズを設定し、リストビューを初期化します(別メソッド)。

(3) 部品棚を作成

インスタンス作成後、DEFクラスのメソッドで部品棚を作成します。

(4) プログレスバーを初期化

部品棚の中の部品数は合計57個あります。それを Maximum にして Value は探索中の部品がコレクション全体の何番目であるかを示すことにしました(Value の計算は後述)。

(5) 探索実行

ここがメインです。DEF.solutionNo は解のカウンターです。探索の本体は nextSearch メソッドです(後述)。前後に経過時間を測るために時刻の取得を挟みます。

(6) 結果表示と終了

途中でキャンセルしなければ終了メッセージを表示し、ユーザー確認後フォームを閉じます。途中キャンセルのときはメッセージを表示しないでそのままフォームを閉じます。

・メソッド:nextSearch(再帰

「探索開始ボタン_Click」から呼ばれる探索の本体部分です。指定の座標位置以降で部品を置くことができる場所を探します。部品が置けたら完成を判定、すべての部品が置けたらパズルの完成です。そうでなければ再帰して次に置く部品を探します。置いた部品数は board.PlacedPieceCount にカウントされます。解は Shelves の各 Shelf 内の PlacedPiece と PlacedCoord に記録しているので完成したものはここからリストビューに表示させます。

private void nextSearch(Board board, List<Shelf> shelves, Coord coord)
{
    if (board.PlacedPieceCount == 1 && board.PlacedFirstPiece != null)
    {
        プログレスバー表示(board.PlacedFirstPiece, shelves);
    }
    Application.DoEvents();
    if (DEF.cancelFlag)
    {
        return;
    }

    coord = board.NextBlankCell(coord);

    foreach (Shelf shelf in shelves)
    {
        if (shelf.PlacedPiece is null)
        {
            foreach (Piece piece in shelf.Pieces)
            {
                if (board.CanPlace(piece, coord))
                {
                    shelf.PlacedPiece = piece;
                    shelf.PlacedCoord = coord;
                    board.PlacePiece(shelf);
                    
                    //完成判定
                    if (board.PlacedPieceCount == shelves.Count)
                    {
                        DEF.solutionNo += 1;
                        リストビュー完成表示(board, shelves);
                    }
                    else
                    {
                        nextSearch(board, shelves, coord);    //再帰
                    }

                    board.RemovePiece(shelf);
                    shelf.PlacedPiece = null;
                    shelf.PlacedCoord = null;
                }
            }
        }
    }
}

・メソッド:リストビュー初期化

解を表示するリストビューを整形します。列数は解Noの表示用に1列余分に取ります。列幅は解No用の列は普通ですが、残りはセルが正方形に見えるように狭く(30px)しています。行数は盤面の行数と同じです。UserItemStyleForSubitems プロパティは False にするとセルごとに背景色を変更できるのでこのプログラムでは必須の設定です。

private void リストビュー初期化(Board board)
{
    LV1.View = View.Details;
    LV1.Font = new Font(LV1.Font.Name, 18);
    LV1.Clear();
    //列追加
    LV1.Columns.Add("", 70);
    for (int col = 0; col < board.Width(); col++)
    {
        LV1.Columns.Add("", 30);
    }
    //フォームの横幅変更
    this.Width = (board.Width() * 30) + 200;
    //行追加
    for (int row = 0; row < board.Height(); row++)
    {
        LV1.Items.Add("");
        LV1.Items[row].UseItemStyleForSubItems = false;
        for (int col = 0; col < board.Width(); col++)
        {
            LV1.Items[row].SubItems.Add("");
        }
    }
    LV1.Refresh();
}

・メソッド:リストビュー完成表示

完成した解をリストビューに表示するメソッドです。解は shelves の各Shelf にあるのでそれをリストビューに転記します。[都度確認あり]のときは「都度確認メソッド」を実行します。

private void リストビュー完成表示(Board board, List<Shelf> shelves)
{
    //LV1.BeginUpdate();
    LV1.Items[0].SubItems[0].Text = DEF.solutionNo.ToString();
    foreach (Shelf shelf in shelves)
    {
        if (shelf.PlacedPiece != null && shelf.PlacedCoord != null)
        {
            Piece piece = shelf.PlacedPiece;
            for (int i = 0; i < piece.Size(); i++)
            {
                int col = shelf.PlacedCoord.X + piece.Coords[i].X + 1;
                int row = shelf.PlacedCoord.Y + piece.Coords[i].Y;
                ListViewItem.ListViewSubItem subItem = LV1.Items[row].SubItems[col];
                subItem.Text = shelf.Name;
                subItem.BackColor = shelf.Color;
                subItem.ForeColor = Color.White;
            }
            //LV1.Refresh();
        }
    }
    if (cbx_都度確認あり.Checked)
    {
        都度確認();
    }
    //LV1.EndUpdate();
}

・メソッド:都度確認

都度確認の継続とプログラムのキャンセルを訊くようにしています。

private void 都度確認()
{
    DialogResult result = MessageBox.Show("完成!!(" + DEF.solutionNo.ToString() + ")" + 
        "\r\n\r\n" + "都度確認を続けますか?", DEF.myName, MessageBoxButtons.YesNoCancel);
    switch (result)
    {
        case DialogResult.Yes:
            break;
        case DialogResult.No:
            cbx_都度確認あり.Checked = false;
            break;
        case DialogResult.Cancel:
            キャンセルボタン.PerformClick();
            break;
    }
}

・メソッド:プログレスバー表示

プログレスバーの値(Value)の計算は、解の最初の部品(board.PlacedFirstPiece)が部品棚のすべての部品の何番目になるのかを計算しています。

private void プログレスバー表示(Piece piece, List<Shelf> shelves)
{
    int val = 0;
    foreach (Shelf shelf in shelves)
    {
        if (shelf.Pieces.Contains(piece))
        {
            val += shelf.Pieces.IndexOf(piece) + 1;
            break;
        }
        else
        {
            val += shelf.Pieces.Count;
        }
    }
    progressBar1.Value = val;
}


以上でクラスからフォームまで、すべてのコードを作り終えました。
実際にプログラムを動かして確認してください。

using System.Reflection;

namespace ペントミノソルバー2D
{
    public partial class Form1 : Form
    {
        //ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
        //Z
        //Z            Form1
        //Z
        //ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

        #region "State(状態遷移)" 

        private int _State = 0;

        private int State
        {
            get
            {
                return _State;  
            }
            set
            {
                _State = value;
                bool[] STS = { value == 0, value == 1, value == 2 };
                groupBox1.Enabled = STS[1];
                cbx_都度確認あり.Enabled = STS[1];
                探索開始ボタン.Enabled = STS[1];
                progressBar1.Visible = STS[2];
                キャンセルボタン.Visible = STS[2];
            }
        }

        #endregion

        //****************************************
        //*          フォームを開くとき
        //****************************************
        public Form1()
        {
            InitializeComponent();
            rbt_10x6.Checked = true;
            EnableDoubleBuffering(LV1);
            State = 1;
        }

        public static void EnableDoubleBuffering(Control control)
        {
            control.GetType().InvokeMember(
               "DoubleBuffered",
               BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty,
               null,
               control,
               new object[] { true });
        }

        //****************************************
        //*         フォームを閉じるとき
        //****************************************
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if(State == 2)
            {
                DialogResult result = MessageBox.Show("中止しますか?","", MessageBoxButtons.YesNo);
                if(result == DialogResult.Yes)
                {
                    DEF.cancelFlag = true;
                }
                e.Cancel = true;
            }
        }

        private void キャンセルボタン_Click(object sender, EventArgs e)
        {
            Close();
        }

        //****************************************
        //*      探索開始ボタンをクリック
        //****************************************
        private void 探索開始ボタン_Click(object sender, EventArgs e)
        {
            State = 2;

            DEF.cancelFlag = false;

            Board board;
            if (rbt_10x6.Checked)
            {
                board = new Board(10, 6);
            }
            else if (rbt_12x5.Checked)
            {
                board = new Board(12, 5);
            }
            else if (rbt_15x4.Checked)
            {
                board = new Board(15, 4);
            }
            else
            {
                board = new Board(20, 3);
            }
            リストビュー初期化(board);

            List<Shelf> shelves = new List<Shelf>();
            DEF.部品棚作成(shelves);

            progressBar1.Maximum = shelves.Sum(s => s.Pieces.Count);
            progressBar1.Minimum = 0;
            progressBar1.Value = 0;

            DateTime startTime = DateTime.Now;
            DEF.solutionNo = 0;
            nextSearch(board, shelves, new Coord(0, 0));
            DateTime endTime = DateTime.Now;

            if (!DEF.cancelFlag)
            {
                progressBar1.Value = progressBar1.Maximum;
                MessageBox.Show("探索終了\r\n\r\n" + DEF.経過時間(startTime, endTime),DEF.myName);
            }

            State = 1;
            Close();
        }

        //****************************
        //*        nextSearch           次を探す(再帰)
        //****************************
        private void nextSearch(Board board, List<Shelf> shelves, Coord coord)
        {
            if (board.PlacedPieceCount == 1 && board.PlacedFirstPiece != null)
            {
                プログレスバー表示(board.PlacedFirstPiece, shelves);
            }
            Application.DoEvents();
            if (DEF.cancelFlag)
            {
                return;
            }

            coord = board.NextBlankCell(coord);

            foreach (Shelf shelf in shelves)
            {
                if (shelf.PlacedPiece is null)
                {
                    foreach (Piece piece in shelf.Pieces)
                    {
                        if (board.CanPlace(piece, coord))
                        {
                            shelf.PlacedPiece = piece;
                            shelf.PlacedCoord = coord;
                            board.PlacePiece(shelf);
                            
                            //完成判定
                            if (board.PlacedPieceCount == shelves.Count)
                            {
                                DEF.solutionNo += 1;
                                リストビュー完成表示(board, shelves);
                            }
                            else
                            {
                                nextSearch(board, shelves, coord);    //再帰
                            }

                            board.RemovePiece(shelf);
                            shelf.PlacedPiece = null;
                            shelf.PlacedCoord = null;
                        }
                    }
                }
            }
        }

        //****************************
        //*     リストビュー関係
        //****************************
        private void リストビュー初期化(Board board)
        {
            LV1.View = View.Details;
            LV1.Font = new Font(LV1.Font.Name, 18);
            LV1.Clear();
            //列追加
            LV1.Columns.Add("", 70);
            for (int col = 0; col < board.Width(); col++)
            {
                LV1.Columns.Add("", 30);
            }
            //フォームの横幅変更
            this.Width = (board.Width() * 30) + 200;
            //行追加
            for (int row = 0; row < board.Height(); row++)
            {
                LV1.Items.Add("");
                LV1.Items[row].UseItemStyleForSubItems = false;
                for (int col = 0; col < board.Width(); col++)
                {
                    LV1.Items[row].SubItems.Add("");
                }
            }
            LV1.Refresh();
        }

        private void リストビュー完成表示(Board board, List<Shelf> shelves)
        {
            //LV1.BeginUpdate();
            LV1.Items[0].SubItems[0].Text = DEF.solutionNo.ToString();
            foreach (Shelf shelf in shelves)
            {
                if (shelf.PlacedPiece != null && shelf.PlacedCoord != null)
                {
                    Piece piece = shelf.PlacedPiece;
                    for (int i = 0; i < piece.Size(); i++)
                    {
                        int col = shelf.PlacedCoord.X + piece.Coords[i].X + 1;
                        int row = shelf.PlacedCoord.Y + piece.Coords[i].Y;
                        ListViewItem.ListViewSubItem subItem = LV1.Items[row].SubItems[col];
                        subItem.Text = shelf.Name;
                        subItem.BackColor = shelf.Color;
                        subItem.ForeColor = Color.White;
                    }
                    //LV1.Refresh();
                }
            }
            if (cbx_都度確認あり.Checked)
            {
                都度確認();
            }
            //LV1.EndUpdate();
        }

        private void 都度確認()
        {
            DialogResult result = MessageBox.Show("完成!!(" + DEF.solutionNo.ToString() + ")" + 
                "\r\n\r\n" + "都度確認を続けますか?", DEF.myName, MessageBoxButtons.YesNoCancel);
            switch (result)
            {
                case DialogResult.Yes:
                    break;
                case DialogResult.No:
                    cbx_都度確認あり.Checked = false;
                    break;
                case DialogResult.Cancel:
                    キャンセルボタン.PerformClick();
                    break;
            }
        }

        private void プログレスバー表示(Piece piece, List<Shelf> shelves)
        {
            int val = 0;
            foreach (Shelf shelf in shelves)
            {
                if (shelf.Pieces.Contains(piece))
                {
                    val += shelf.Pieces.IndexOf(piece) + 1;
                    break;
                }
                else
                {
                    val += shelf.Pieces.Count;
                }
            }
            progressBar1.Value = val;
        }

    }
}