コントロールの動的生成を使った、お試しプログラムを書き殴る用のプロジェクト

普段、動作確認などでちょっとしたテスト用プログラムを良く書きます。
ユニットテストなどではなく、単に使い方を確認したりするためだけの殴り書きレベルです。

1個の処理は、大抵は数行～数十行程度なのですが、それが数十種類あり、個別にプロジェクトを作ったりするとプロジェクトだらけになってしまうので、お試しプログラム用のプロジェクトを作ってまとめています。

フォームにボタンをペタペタ貼って、そこから処理を呼ぶだけのお手軽設計ですが、こうしておくと、一度作った処理をクリックするだけで何度でも試せるし、書いた処理がそのまま備忘録になって便利です。

しかし、最近フォームにボタンを貼り過ぎてしまいデザイナーがかなりもっさりしてしまいました。
もう一つの難点として、だんだんボタンの置き場所が減ってきてしまい、場所を確保するために並べ直したりするのが地味に面倒です。

そこで、書きたい処理を書くとGUIを自動的に生成してくれるひな型アプリを作りました。


今回作るのは書き殴るプログラムを入れるためのアプリの側なので中身はスカスカですが、逆に余計な処理がないので初めてC# でGUI アプリを作る人にも分かりやすいのではないでしょうか。

ちなみに、今回のアプリで行っているフォームのボタンをコードで生成するような、デザイナーで行っている操作をコードで実装したい場合は、フォームのデザイナーで自動生成されるコードをコピペして参考にすると簡単です。

# 開発環境
Visual Studio 2022 Comunity

# 構成
分類ができるように、こんな感じでカテゴリーと項目という2階層構造にしました。
```
├カテゴリーA
│      ├項目1
│      ├項目2
│      ・
│      ・
│      ・
│      └項目x
│
├カテゴリーB
│      ├項目1
│      └項目2
│
・
・
・
```


# プロジェクトの作成
C# のWindows フォームアプリ プロジェクトを作成します。
フレームワークは.NET 7.0 を使用しましたが、.NET Framework などでも同じです。

# フォームのデザイン
フォームはこんな感じで、左側にカテゴリーを選択するListBox、右側にその内訳の項目を表示するFlowLayoutPanel を並べました。

![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527645/3555e3c6-6e37-389b-9b7d-aed85eeae7ce.png)

ListBox は最低限Anchor を指定しておくくらいでOKですが、そのままだと項目の高さが低く、項目が詰まりすぎてしまうので、高さを変更するためにDrawMode=OwnerDrawFixed にしてItemHeight=30 で高さを指定したうえで、自分で描画(オーナードロー)しています。


FlowLayoutPanel は、ボタンなどのコントロールを追加すると自動的にレイアウトしてくれるので、コントロールを表示する際に座標計算をする必要がなくなります。
また、AutoScroll=true を指定しておくと、コントロールが収まらなかった場合に自動的にスクロールバーが表示されるようになります。

デザイナーのソースはこんな感じです。
``` CSharp:FormMain.Designer.cs
    private void InitializeComponent()
    {
        ListBoxカテゴリー = new ListBox();
        FlowLayoutPanel項目 = new FlowLayoutPanel();
        SuspendLayout();
        // 
        // ListBoxカテゴリー
        // 
        // フォームがリサイズされた時に縦方向のみ追従
        ListBoxカテゴリー.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left;
        // オーナードロー(後述)
        ListBoxカテゴリー.DrawMode = DrawMode.OwnerDrawFixed;
        ListBoxカテゴリー.FormattingEnabled = true;
        // リストアイテムの高さに足りない状態でも描画されるようにしてきちんとフォームの高さに追従するようにする
        ListBoxカテゴリー.IntegralHeight = false;
        // 項目の高さを指定(ItemHeightを指定する場合は、DrawModeの指定が必須)
        ListBoxカテゴリー.ItemHeight = 30;
        ListBoxカテゴリー.Location = new Point(12, 12);
        ListBoxカテゴリー.Name = "ListBoxカテゴリー";
        ListBoxカテゴリー.Size = new Size(182, 426);
        ListBoxカテゴリー.TabIndex = 1;
        // 今回はオーナードローで自分で描画するので描画処理のイベント
        ListBoxカテゴリー.DrawItem += ListBoxカテゴリー_DrawItem;
        // 項目が選択されたときの処理
        ListBoxカテゴリー.SelectedIndexChanged += ListBoxカテゴリー_SelectedIndexChanged;
        // 
        // FlowLayoutPanel項目
        // 
        // フォームがリサイズされた時に縦と横方向に追従
        FlowLayoutPanel項目.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
        // 表示するボタンがパネルのサイズに収まらない場合、スクロールバーを表示
        FlowLayoutPanel項目.AutoScroll = true;
        FlowLayoutPanel項目.Location = new Point(200, 12);
        FlowLayoutPanel項目.Name = "FlowLayoutPanel項目";
        FlowLayoutPanel項目.Size = new Size(588, 426);
        FlowLayoutPanel項目.TabIndex = 2;
        // 
        // FormMain
        // 
        AutoScaleDimensions = new SizeF(7F, 15F);
        AutoScaleMode = AutoScaleMode.Font;
        ClientSize = new Size(800, 450);
        Controls.Add(FlowLayoutPanel項目);
        Controls.Add(ListBoxカテゴリー);
        Name = "FormMain";
        Text = "汎用テスト";
        Load += FormMain_Load;
        ResumeLayout(false);
    }

    private ListBox ListBoxカテゴリー;
    private FlowLayoutPanel FlowLayoutPanel項目;
```

# カテゴリーと項目を表す抽象クラス
カテゴリークラスには、項目のリストを作っておきます。
今回はクラス名をそのままリストボックスの項目名にするので、ToString で型名を返します。
ただし、ListBox ではItem の描画のたびにToString が呼ばれることになるので、一旦変数で受けてキャッシュしておきます。
``` CSharp:カテゴリーBase
internal abstract class カテゴリーBase
{
    private readonly string Title;
    protected カテゴリーBase() => Title = GetType().Name;

    public List<項目Base> Items { get; } = new List<項目Base> { };

    public override string ToString() => Title;
}
```

項目クラスには実際の処理が書かれているExecute を呼ぶためのボタンを生成する機能を付けておきます。
このクラスを継承してExecute に好きな処理を書くことになります。

``` CSharp:項目Base
internal abstract class 項目Base
{
    public Button GetButton()
    {
        var name = GetType().Name;
        var result = new Button()
        {
            Name = $"Button{name}",
            Size = new Size(225, 46),
            Text = name,
            UseVisualStyleBackColor = true,
        };
        result.Click += Execute;
        return result;
    }
    private void Execute(object? sender, EventArgs e) => Execute((Button)sender!, (MouseEventArgs)e);
    public abstract void Execute(Button sender, MouseEventArgs e);
}
```

# 使い方
こんな感じで書きます。
名前の衝突を考えるとネームスペースを分けるのがいいでしょう。

クラス名が自動的に項目名になるので名前など入力する必要はなく
1)継承する
2)処理を書く
だけでOKです。

``` CSharp
namespace カテゴリーA;
internal class カテゴリーA : カテゴリーBase
{
    public カテゴリーA()
        => Items.AddRange(new 項目Base[]
        {
            new 項目1(),
            new 項目2(),
        });
}

internal class 項目1 : 項目Base
{
    public override void Execute(Button sender, MouseEventArgs e) => MessageBox.Show("Call 項目1");
}

internal class 項目2 : 項目Base
{
    public override void Execute(Button sender, MouseEventArgs e) => MessageBox.Show("Call 項目2");
}
```

# フォームの実装
フォームで必要なのはリストボックスの描画処理とリストボックスが選択された時の処理です。
``` CSharp:FormMain.cs
public partial class FormMain : Form
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    public FormMain() => InitializeComponent();

    /// <summary>
    /// フォームLoad時の処理
    /// </summary>
    private void FormMain_Load(object sender, EventArgs e)
    {
        // リストボックスをクリア
        ListBoxカテゴリー.Items.Clear();

        // ここに追加したいカテゴリーを追加していく
        ListBoxカテゴリー.Items.AddRange(new カテゴリーBase []
        {
            new カテゴリーA.カテゴリーA(),
            new カテゴリーB.カテゴリーB()
        });


        // 最初の項目を選択
        ListBoxカテゴリー.SelectedIndex = 0;
    }

    /// <summary>
    /// カテゴリーSelectIndexChanged時の処理
    /// </summary>
    private void ListBoxカテゴリー_SelectedIndexChanged(object sender, EventArgs e)
    {
        var panel = FlowLayoutPanel項目;
        // パネル内のコントロールをクリア
        panel.Controls.Clear();

        // リストボックスの選択項目からカテゴリーを取得
        var target = ((ListBox)sender).SelectedItem;
        if (target is not カテゴリーBase targetItem) return;

        // カテゴリー内の項目からボタン取得してパネルに追加
        foreach (var item in targetItem.Items)
        {
            panel.Controls.Add(item.GetButton());
        }
    }

    /// <summary>
    /// カテゴリーDrawItem時の処理
    /// </summary>
    /// <remarks>オーナードローの処理</remarks>
    private void ListBoxカテゴリー_DrawItem(object sender, DrawItemEventArgs e)
    {
        //背景を描画する
        e.DrawBackground();

        //ListBoxが空のときにListBoxが選択されるとe.Indexが-1になる
        if (0 <= e.Index)
        {
            //文字を描画する色の選択
            var b = new SolidBrush(e.ForeColor);
            //描画する文字列の取得
            var txt = ((ListBox)sender).Items[e.Index].ToString();
            //文字列の描画(上下中央で描画)
            e.Graphics.DrawString(txt, e.Font!, b, e.Bounds, new StringFormat() { LineAlignment = StringAlignment.Center });
        }

        //フォーカスを示す四角形を描画
        e.DrawFocusRectangle();
    }
}
```

ListBoxカテゴリー_DrawItem がオーナードローの処理で、リストボックスの中身が描画されるときに呼ばれます。
ここでは、アイテムから描画する文字を取得して、Item の枠内に上下中央になるように描画しています。
[ListBox.DrawItem イベント (System.Windows.Forms)](https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.listbox.drawitem?view=windowsdesktop-7.0)

# 実行の様子
左側のリストボックスからカテゴリーを選択すると、右側に項目のボタンが表示されます。
![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527645/007137cf-196f-6a8e-430c-b16068a43f1c.png)

リサイズするとボタンが自動的に再配置され、配置しきれないときはスクロールバーが表示されます。
![008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527645/4cec2698-b8cf-812b-b294-8986b1fb2c01.png)


# まとめ
こういった用途向けに便利なフレームワークなどもたくさんありますが、これくらいシンプルなものでも作っておくと、気になったことを気軽に試せて、それを残しておくことができるようになるので、一つあると何かと便利です。