VS2022デザイン時C#プログラミング(WinForms)

この記事は「[C#アドベントカレンダー](https://qiita.com/advent-calendar/2021/csharplang) 」18日目です。

##はじめに

皆様WinForms使ってますか？(^^)
**私は当面、手を切れそうにありません...**
そんな訳で、普段仕事で使っている~~手抜き~~省力化テクを、VS2022&NET6でも使えるかの検証を兼ねて記事化します。

まぁ仕事的には暫く**NET6にも進めそうにない**ですが(^^;

## この記事で実現できる事

IDE上のフォームデザインの自動化・省力化を実現します。  
Visual Studio 2022(※)上のWinFormsアプリの<font color="Red">**デザイン時**</font>に

- 設定ファイルを読み込み、
- コントロールの追加、または
- コントロールの設定の変更

...を行います。

※恐らく2017,2019でもusingの変更程度で動くと思います。

<まっさらなForm1に...>
![20211216_qiita_img01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/79381b67-1c7a-7690-06fb-12382ed3c09c.png)

<パパっとコントロールが作成される！>
![20211216_qiita_img02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/67371f63-c0de-0bd2-c29f-cdbcda0377cb.png)

手作業でコントロールの追加やプロパティの変更をチマチマやる代わりに<font color="Red">**C#に頑張って貰おう**</font>というお話です。

## いざ作成！...の準備

### (1)新しいWinFormsプロジェクトを作成

本記事では**NET6**で作成しています。

### (2)自作コンポーネント用ファイル(以下"MyComponent")を追加

作成したアプリケーションで右クリック、追加→クラスで"MyComponent"(名前なんでも良いです)を追加します(MyComponent.csが作成されます)。
<font color="Red">**このファイルに各挙動を追加して行きます。**</font>

### (3)MyComponentの準備

```C#:MyComponent.cs

//要追加のusing(※足りなかったらゴメンナサイ。VSのお勧めに従い追加して下さい)
using System.ComponentModel; 
using System.ComponentModel.Design;

//折角NET6なので{}括らない(クラスのインデント下げない)方式で
namespace WinFormsApp1; //ここは作成したプロジェクト名次第

//Componentを継承したMyComponentを作成
public class MyComponent : Component
{
    private bool _exec;

    [Browsable(true)]
    [Description("設定ファイルを指定します。")]
    [Category("設定")]
    public string FilePath { get; set; } = "";

    [Browsable(true)]
    [Description("デザイン時、設定ファイルをフォームに反映します。")]
    [Category("設定")]
    public bool Exec
    {
        get => _exec;
        set
        {
            try
            {
                if (!DesignMode || Container == null || !value)
                {
                    //デザインモード or コンテナがNULL or 実行じゃない、抜ける
                    return;
                }

                //(出力コンソールには出せなさそうなので、ダイアログ表示でデバッグする)
                //MessageBox.Show(Container?.ToString() ?? "(null)");

                IDesignerHost? host = Container as IDesignerHost;

                if (host == null)
                {
                    //IDesignerHostが取得できない、抜ける
                    return;
                }

                Form? form = host.RootComponent as Form;

                if (form == null)
                {
                    //フォームが取得できない、抜ける
                    return;
                }

                ////ここに実装する処理を書いていく

            }
            finally
            {
                //最後、必ずfalseにする
                _exec = false;
            }

        }
    }
}
```

...という形で作成して、一度"実行"します。
コンパイルが成功して空のフォーム(Form1)が表示されればOKです。

この後FilePathプロパティに設定ファイルのフルパスを設定して、
<font color="Red">**Execプロパティを変更すると処理実行**</font>となります。
因みにExecプロパティがtrueになる事はありません。セッターをC#実行のトリガーとしています。

※本来は右クリックで出てくるメニューから実行させるのが正しいお作法なんでしょうけど、~~そこまでするの面倒~~お手軽に実行できる方法という事で...(売り物でもないのでね...)

### (4)設定ファイルの準備

```Form1.txt:UTF-8で保存してね！適当な設定ファイル
X,Y座標,項目名Eng,Jpn,MaxLength
10,50,field01,項目１,30
10,80,field02,項目２だ,30
10,110,field03,項目３ですん,30
10,140,field04,項目４,30
10,170,field05,項目５だべ～～,30
410,50,field06,項目６どす,30
410,80,field07,項目７や,30
410,110,field08,項目８♪,30
410,140,field09,項目９ぅぅ,30
410,170,field10,項目１０っ,30
```

今回は上記設定ファイルを基にLabelとTextBoxを自動追加・更新します。
設定ファイルには基準となるXY座標、項目名(英日)、TextBoxのMaxLengthを記述します。

適当なフォルダ・ファイル名で**UTF-8で**保存します(ここではForm1.txt)。

### (5)MyComponentをForm1に貼り付け

"Form1.cs\[デザイン]"タブを選択すると、ツールボックスに"MyComponent"が増えているはずです。

![20211216_qiita_img03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/a4ce7b4f-0c9d-21ec-1d96-cf337a9edff2.png)

MyComponentをDropして"FilePath"に設定ファイルのフルパスを指定しておきます。

ここから先、"////ここに実装する処理を書いていく"部分を埋めて行く訳ですが、

<font color="Red">**１．MyComponentの修正**</font>
　↓
<font color="Red">**２．MyComponentのコンパイル**</font>(成功する必要あり)
　↓
<font color="Red">**３．MyComponentの実行**</font>→Formへの反映

...という感じに、**先にコンパイル(要成功)→MyComponent実行**の流れで進める必要があります。

デバッグ時も同様に、**先ずコンパイルが通る状態にする→MessageBox.Showで見たい内容を記述→コンパイル→MyComponent実行でメッセージを確認する**感じになります。

それに対してExec以外のプロパティ(ここでは"FilePath"しかないですが)の変更は、先コンパイル不要です。Exec時に直ぐに反映されます。

## いざ実装！

### フォームにコントロールを追加する

```C#:MyComponent.csの"////ここに実装する処理を書いていく"以下に記述する

    //設定ファイル読み込み
    string[] lines = File.ReadAllLines(FilePath);

    //2行目からループ
    foreach (var line in lines.Skip(1))
    {
        //適当にパース
        string[] vals = line.Split(',');

        int x = int.Parse(vals[0]);
        int y = int.Parse(vals[1]);
        string eng = vals[2];
        string jpn = vals[3];
        int maxLen = int.Parse(vals[4]);

        //Label生成
        Label label = new Label();
        label.Parent = form;
        label.AutoSize = true;
        label.Text = jpn;
        label.Left = x;
        label.Top = y;

        //フォームにLabel追加(※label.Nameはこの第2引数でしか設定できない)
        Container.Add(label, $"{eng}Label");

        //TextBox生成
        TextBox textBox = new TextBox();
        textBox.Parent = form;
        textBox.Text = jpn;
        textBox.Left = x + 100;
        textBox.Top = y;
        textBox.Width = 160;
        textBox.MaxLength = maxLen;

        //フォームにTextBox追加(※textBox.Nameはこの第2引数でしか以下略)
        Container.Add(textBox, $"{eng}TextBox");
    }

```

継承したComponentのContainerに、ここで生成したLabel,TextBoxを追加します。

※生成したインスタンスを(Parentに代入した)formに追加すると、どうなるか！？　気になった人は試してみて下さい(^^)

(しつこいですが)<font color="Red">**ここでコンパイルして成功した後**</font>に<font color="Red">**Execプロパティを変更**</font>すると...

(再掲)
![20211216_qiita_img02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/67371f63-c0de-0bd2-c29f-cdbcda0377cb.png)

まっさらだったForm1上にコントロール群が追加されました！
例えば"項目２だ"のMaxLengthを見てみると、30に設定されています。

![20211216_qiita_img04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/2a64fe0e-8394-fc53-b2e0-4fd423c5dfd5.png)

### フォームに配置済みのコントロールを更新する

例えば仕様変更があって、下記のように設定ファイルを修正した場合ですね。

```Form1.txt:UTF-8で保存してね！適当な設定ファイル(変更・追加分を抜粋)
X,Y座標,項目名Eng,Jpn,MaxLength
10,80,field02,項目２だったよ,100
10,140,field04,項目４よん,30
410,50,field06,項目６どすぇ,30
410,140,field09,項目９ぅ,30
10,200,field11,項目１１だっ,100
```

ソースの方は、

```C#:MyComponent.csの"////ここに実装する処理を書いていく"以下に記述する

    //設定ファイル読み込み
    string[] lines = File.ReadAllLines(FilePath);

    //form漁ってタプル返す内部関数
    (Label? lbl, TextBox? txb) searchForm(string eng)
    {
        Label? lbl = (Label?)form.Controls.Find($"{eng}Label", true).FirstOrDefault();
        TextBox? txb = (TextBox?)form.Controls.Find($"{eng}TextBox", true).FirstOrDefault();

        return (lbl, txb);
    }

    //2行目からループ
    foreach (var line in lines.Skip(1))
    {
        //適当にパース
        string[] vals = line.Split(',');

        int x = int.Parse(vals[0]);
        int y = int.Parse(vals[1]);
        string eng = vals[2];
        string jpn = vals[3];
        int maxLen = int.Parse(vals[4]);

        //Label,TextBox存在チェック
        (Label? label, TextBox? textBox) = searchForm(eng);

        //Label(なければ)生成
        bool add = label == null;
        label ??= new Label() 
        { 
            Parent = form,
            AutoSize = true,
        };

        //Label設定
        label.Text = jpn;
        label.Left = x;
        label.Top = y;

        //生成時はフォームにLabel追加(※label.Nameはこの第2引数でしか設定できない)
        if (add)
        {
            Container.Add(label, $"{eng}Label");
        }

        //TextBox(なければ)生成
        add = textBox == null;
        textBox ??= new TextBox() 
        { 
            Parent = form,
            Width = 160,
        };

        //TextBox設定
        textBox.Text = jpn;
        textBox.Left = x + 100;
        textBox.Top = y;
        textBox.MaxLength = maxLen;

        //フォームにTextBox追加(※textBox.Nameはこの第2引数でしか以下略)
        if (add)
        {
            Container.Add(textBox, $"{eng}TextBox");
        }
    }

```

...な感じに修正して、設定の追加・更新に対応しました。

コンパイルし直して実行すると、変更が反映されています！

![20211216_qiita_img05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/3911692a-5d02-b3cd-30f9-6bbb2866b3c8.png)

例えば"項目２だったよ"のMaxLengthも100に変更されました。

![20211216_qiita_img06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954879/4ae46d57-ba89-6b91-cbd6-70c510f32083.png)

コードの修正点は、searchForm内部関数で生成済みのLabel,TextBoxを探して、

- 見つかった場合はインスタンスを更新
- なければ生成＆Containerに追加

...です。

ポイントは、**生成済みのコントロールの場合はプロパティの変更が直ちに反映される**という所。
なので例えば自作コントロールの場合は、FindForm()で親フォームから配下のコントロールを弄る事ができたりします♪

## まとめ

という事で、

- コントロールを追加する
- コントロールを追加・更新する

...の2パターンを作成しました。

弊社では今の所"コントロールを更新する"部分(追加はしない)で活用しています。

"実際にどのように配置するか？"とか、"どのような項目・コントロールを対象とするか？"は個々の事情により異なると思いますが、C#で操作できるという部分で<font color="Red">**自由度はかなり高い**</font>です！

大量に似たような画面を作成・メンテナンスするシチュエーションで活躍するかと思います(マスメン画面とか...)。

少しでも、どなたかの参考になれば幸いです(^^)

## 余談

"FilePathプロパティを作る代わりにOpenFileDialogを使っても良いです"とか書きそうになって、事前に試してみたら**Visual Studio 2022が固まりました(^^;**

なので100％何でも動かせるという訳ではありませんが、コントロールを弄ろうという範疇なら問題なく遊べるのではないかと思います！