C#で将棋盤を描画してみようその2.3 ～駒の描画3～

[その2.2](http://qiita.com/pizyumi/items/89fadf15c35451f58a34)からの続き。

# その8 描画内容

前回作成した描画内容を表すクラスに枡に関する項目を追加します。取り敢えず今回は枡の描画内容としては駒の画像のみとしています。駒の画像は複数個指定できるようにしました（将棋のルール上1つの枡に複数の駒が存在することはあり得ないが、物理的には複数存在することもあり得るため一応複数個指定できるようにしておきました）。

```csharp
    //描画内容
    public class Content
    {
        //行数
        public int NumOfRows { get; set; }
        //列数
        public int NumOfColumns { get; set; }

        //枡の幅
        public int Width { get; set; }
        //枡の高さ
        public int Height { get; set; }

        //線の太さ
        public int Thickness { get; set; }
        //線の色
        public Color Color { get; set; }

        //枡
        public Cell[,] Cells { get; set; }
    }

    //枡
    public class Cell
    {
        //駒の画像
        public IEnumerable<Option<Image>> PieceImages { get; set; }
    }
```

# その9 描画内容に関する処理

前回作成したプログラムで将棋に関係のある部分（CreateDefaultContent関数）をSyougiクラスに移動します。将棋のルールに係る定数（行数と列数）は関数の外に出しました。また、枡の描画内容を設定する処理を追加しました。

```csharp
    //将棋に関する処理
    public class Syougi
    {
        //行数
        public static readonly int NumOfRows = 9;
        //列数
        public static readonly int NumOfColumns = 9;

        //既定の描画内容を作成し、返す
        private static Content CreateDefaultContent()
        {
            //既定の枡の幅
            const int width = 40;
            //既定の枡の高さ
            const int height = 40;
            //既定の線の太さ
            const int thickness = 1;
            //既定の線の色の赤成分
            const int red = 0;
            //既定の線の色の緑成分
            const int green = 0;
            //既定の線の色の青成分
            const int blue = 0;

            //既定の線の色
            Color color = Color.FromArgb(red, green, blue);

            //枡
            Cell[,] cells = new Cell[NumOfRows, NumOfColumns];
            for (int i = 0; i < NumOfRows; i++)
                for (int j = 0; j < NumOfColumns; j++)
                    cells[i, j] = new Cell();

            //既定の描画内容を作成する
            Content content = new Content();
            //既定の描画内容を設定する
            content.NumOfRows = NumOfRows;
            content.NumOfColumns = NumOfColumns;
            content.Width = width;
            content.Height = height;
            content.Thickness = thickness;
            content.Color = color;
            content.Cells = cells;

            //既定の描画内容を返す
            return content;
        }
    }
```

描画内容に関する他の処理をSyougiクラスに追加します。

描画内容と描画資源と局面を引数として受け取って描画内容を更新する関数を追加します。

```csharp
        //描画内容と描画資源と局面を受け取って描画内容を更新する
        private static void UpdateContent(Content content, Resource resource, Space space)
        {
            //描画内容の枡
            Cell[,] cells = content.Cells;
            //局面の枡
            Section[,] sections = space.Sections;

            //全ての枡について描画内容を更新する
            for (int i = 0; i < NumOfRows; i++)
                for (int j = 0; j < NumOfColumns; j++)
                    //局面の枡に駒が存在しない場合には描画内容の枡の駒の画像は空にする
                    //存在する場合には駒に対応する駒の画像にする
                    cells[i, j].PieceImages = sections[i, j].Piece.Match(
                        none: () => new Option<Image>[] { },
                        some: (_) => new Option<Image>[] { GetImage(resource, _) }
                    );
        }
```

# その10 初期化処理

ここまでで作成した関数を組み合わせて初期化処理を行う関数をSyougiクラスに追加します。初期化処理としては初形の局面を描画するための描画内容の作成を含めます（今回は初形の局面のみを描画します）。

その前に、拡張メソッドを追加します。

```csharp
    //共通の拡張関数
    public static class CommonExtension
    {
        //パイプ
        public static void Pipe<T>(this T self, Action<T> action)
        {
            action(self);
        }
    }
```

初期化処理を行う関数を追加します。この関数は外部から呼び出すことができます。Syougiクラスの内容として描画内容や描画資源や局面も追加します。描画内容のみ外部から参照可能とします。

```csharp
        //描画内容
        public Content Content { get; private set; }

        //描画資源
        private Resource Resource { get; set; }
        //局面
        private Space Space { get; set; }

        //初期化処理を行う
        public bool Initialize()
        {
            //画像が格納されているフォルダの場所
            const string folder = @"C:\work\syougi";

            //描画資源を作成する
            Resource = CreateResource(folder);

            //局面を作成する
            Space = CreateSpace();
            //局面を初形の局面に変更する
            Space.Pipe((_) => InitializeSpace(_));

            //既定の描画内容を作成する
            Content = CreateDefaultContent();
            //描画内容を更新する
            Content.Pipe((_) => UpdateContent(_, Resource, Space));

            return true;
        }
```

# その10 Syougiクラスの使用及び描画処理

最後です。ここまでで作成したSyougiクラスを使用するように前回のプログラムを変更し、描画処理に駒の画像を描画する処理を追加します。また、将棋に関する処理の初期化に失敗した場合には運用記録を発行し、メッセージボックスも表示します。将棋盤の描画も行いません。駒の画像を描画する際には最初に全ての枡の範囲を計算してしまってから画像（又は画像が存在しない場合に画像の不存在を示す×印）を描画します。

```csharp
        //ここからプログラムの実行が始まる
        [STAThread]
        public static void Main()
        {
            //フォーム名
            const string title = "クレアたん将棋";
            //フォームの幅
            const int width = 800;
            //フォームの高さ
            const int height = 600;

            //将棋に関する処理を含むクラスを作成する
            using (Syougi syougi = new Syougi())
            //将棋盤を描画するためのフォームを作成する
            using (Form form = new Form())
            {
                //将棋に関する処理の初期化を行う
                bool isInitialized = syougi.Initialize();

                //初期化に失敗した場合
                if (!isInitialized)
                {
                    //過誤の運用記録を出力する
                    const string id = "59b90902-fd75-4c9e-8d29-6eb6d770ebb5";
                    const string text = "failed initialize syougi";
                    const LogType type = LogType.Error;

                    Logger.IssueLog(new LogItem(Guid.Parse(id), text, type, FormalName));

                    //メッセージボックスを表示する
                    MessageBox.Show("初期化に失敗しました。", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }

                //フォーム名を設定する
                form.Text = title;
                //フォームの大きさを設定する
                form.Width = width;
                form.Height = height;

                //初期化に成功した場合
                //初期化に失敗した場合には何も描画しない
                if (isInitialized)
                    //描画を実行しなければならない時
                    //将棋盤を描画する
                    form.Paint += (sender, e) =>
                    {
                        //描画環境を作成する
                        Context context = new Context();
                        context.G = e.Graphics;
                        //描画範囲はフォームのクライアント領域の全面
                        context.Range = new Range2d<int>(0, 0, form.ClientSize.Width, form.ClientSize.Height);

                        //描画を実行する
                        Draw(context, syougi.Content);
                    };

                //アプリケーションの実行を開始する
                Application.Run(form);
            }
        }

        //描画を実行する
        private static void Draw(Context context, Content content)
        {
            //描画手段
            Graphics g = context.G;
            //描画範囲
            Range2d<int> range = context.Range;

            //描画するものが存在しない場合に描画する×印の線の色
            Pen notFoundPen = Pens.Red;

            //描画範囲外に描画しようとしても描画されないようにする
            g.SetClip(new Rectangle(range.X, range.Y, range.Width, range.Height));

            //枡の境界の線を描画しなければならない場合
            if (content.Thickness != 0 && content.Color != Color.Empty && content.Color != Color.Transparent)
            {
                //枡と枡の境界の全体の幅
                int width = content.Width * content.NumOfColumns + content.Thickness * (content.NumOfColumns + 1);
                //枡と枡の境界の全体の高さ
                int height = content.Height * content.NumOfRows + content.Thickness * (content.NumOfRows + 1);

                //線を描画するための単色のブラシを作成する
                using (SolidBrush brush = new SolidBrush(content.Color))
                {
                    //縦線を描画する
                    for (int i = 0, x = range.X; i <= content.NumOfColumns; i++, x += content.Thickness + content.Width)
                        g.FillRectangle(brush, x, range.Y, content.Thickness, height);
                    //横線を描画する
                    for (int i = 0, y = range.Y; i <= content.NumOfRows; i++, y += content.Thickness + content.Height)
                        g.FillRectangle(brush, range.X, y, width, content.Thickness);
                }
            }

            //枡の範囲を計算する
            Range2d<int>[,] cellRanges = new Range2d<int>[content.NumOfRows, content.NumOfColumns];
            for (int j = 0, x = range.X + content.Thickness; j < content.NumOfColumns; j++, x += content.Width + content.Thickness)
                for (int i = 0, y = range.Y + content.Thickness; i < content.NumOfRows; i++, y += content.Height + content.Thickness)
                    cellRanges[i, j] = new Range2d<int>(x, y, content.Width, content.Height);

            for (int i = 0; i < content.NumOfRows; i++)
                for (int j = 0; j < content.NumOfColumns; j++)
                    //全ての駒の画像を描画する
                    foreach (var image in content.Cells[i, j].PieceImages)
                        image.Match(
                            //画像が存在しない場合には×印を描画する
                            none: () =>
                            {
                                g.DrawLine(Pens.Red, cellRanges[i, j].X, cellRanges[i, j].Y, cellRanges[i, j].X + content.Width - 1, cellRanges[i, j].Y + content.Height - 1);
                                g.DrawLine(Pens.Red, cellRanges[i, j].X + content.Width - 1, cellRanges[i, j].Y, cellRanges[i, j].X, cellRanges[i, j].Y + content.Height - 1);
                            },
                            //存在する場合には画像を描画する
                            some: (_) => g.DrawImage(_, cellRanges[i, j].X, cellRanges[i, j].Y, content.Width, content.Height)
                        );
        }
```

# 実行結果

## 駒の画像を格納するフォルダがない場合

画像が表示されるべき場所には画像の代わりに×印が描画されています。

![creatans03.png](https://qiita-image-store.s3.amazonaws.com/0/10969/016d1b37-3c5c-fdb5-ea9f-2f14b0872ce7.png)

ログとして`not found folder`が出力されています。

![creatans04.png](https://qiita-image-store.s3.amazonaws.com/0/10969/97171f35-53d2-bcce-953e-c5a16c5978a8.png)

## 駒の画像を格納するフォルダはあるがファイルが存在しない場合

画像が表示されるべき場所には画像の代わりに×印が描画されています。

![creatans03.png](https://qiita-image-store.s3.amazonaws.com/0/10969/016d1b37-3c5c-fdb5-ea9f-2f14b0872ce7.png)

ログとして`not found file`が出力されています。

![creatans05.png](https://qiita-image-store.s3.amazonaws.com/0/10969/b44f6a17-44ae-4b7b-382f-9f656ceff6ee.png)

## ファイルが存在する場合

画像が表示されるべき場所には適切な画像が描画されています。ただし、画像ファイルとして読み込めなかった場合には画像の代わりに×印が描画されています。

![creatans06.png](https://qiita-image-store.s3.amazonaws.com/0/10969/dd7cb486-6c8a-9e0d-0ce5-4f42f47cd6b5.png)

ログとして`bad format image`が出力されています（`not found file`も出力されているのは成駒の画像を用意していないためです）。

![creatans07.png](https://qiita-image-store.s3.amazonaws.com/0/10969/ac96f878-97e2-f5b2-75aa-37256e165877.png)