この記事は「C#アドベントカレンダー 」18日目です。
##はじめに
皆様WinForms使ってますか?(^^)
私は当面、手を切れそうにありません...
そんな訳で、普段仕事で使っている手抜き省力化テクを、VS2022&NET6でも使えるかの検証を兼ねて記事化します。
まぁ仕事的には暫くNET6にも進めそうにないですが(^^;
この記事で実現できる事
IDE上のフォームデザインの自動化・省力化を実現します。
Visual Studio 2022(※)上のWinFormsアプリのデザイン時に
- 設定ファイルを読み込み、
- コントロールの追加、または
- コントロールの設定の変更
...を行います。
※恐らく2017,2019でもusingの変更程度で動くと思います。
手作業でコントロールの追加やプロパティの変更をチマチマやる代わりにC#に頑張って貰おうというお話です。
いざ作成!...の準備
(1)新しいWinFormsプロジェクトを作成
本記事ではNET6で作成しています。
(2)自作コンポーネント用ファイル(以下"MyComponent")を追加
作成したアプリケーションで右クリック、追加→クラスで"MyComponent"(名前なんでも良いです)を追加します(MyComponent.csが作成されます)。
このファイルに各挙動を追加して行きます。
(3)MyComponentの準備
//要追加の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プロパティに設定ファイルのフルパスを設定して、
Execプロパティを変更すると処理実行となります。
因みにExecプロパティがtrueになる事はありません。セッターをC#実行のトリガーとしています。
※本来は右クリックで出てくるメニューから実行させるのが正しいお作法なんでしょうけど、そこまでするの面倒お手軽に実行できる方法という事で...(売り物でもないのでね...)
(4)設定ファイルの準備
X,Y座標,項目名Eng,Jpn,MaxLength
10,50,field01,項目1,30
10,80,field02,項目2だ,30
10,110,field03,項目3ですん,30
10,140,field04,項目4,30
10,170,field05,項目5だべ~~,30
410,50,field06,項目6どす,30
410,80,field07,項目7や,30
410,110,field08,項目8♪,30
410,140,field09,項目9ぅぅ,30
410,170,field10,項目10っ,30
今回は上記設定ファイルを基にLabelとTextBoxを自動追加・更新します。
設定ファイルには基準となるXY座標、項目名(英日)、TextBoxのMaxLengthを記述します。
適当なフォルダ・ファイル名でUTF-8で保存します(ここではForm1.txt)。
(5)MyComponentをForm1に貼り付け
"Form1.cs[デザイン]"タブを選択すると、ツールボックスに"MyComponent"が増えているはずです。
MyComponentをDropして"FilePath"に設定ファイルのフルパスを指定しておきます。
ここから先、"////ここに実装する処理を書いていく"部分を埋めて行く訳ですが、
1.MyComponentの修正
↓
2.MyComponentのコンパイル(成功する必要あり)
↓
3.MyComponentの実行→Formへの反映
...という感じに、先にコンパイル(要成功)→MyComponent実行の流れで進める必要があります。
デバッグ時も同様に、先ずコンパイルが通る状態にする→MessageBox.Showで見たい内容を記述→コンパイル→MyComponent実行でメッセージを確認する感じになります。
それに対してExec以外のプロパティ(ここでは"FilePath"しかないですが)の変更は、先コンパイル不要です。Exec時に直ぐに反映されます。
いざ実装!
フォームにコントロールを追加する
//設定ファイル読み込み
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に追加すると、どうなるか!? 気になった人は試してみて下さい(^^)
(しつこいですが)ここでコンパイルして成功した後にExecプロパティを変更すると...
まっさらだったForm1上にコントロール群が追加されました!
例えば"項目2だ"のMaxLengthを見てみると、30に設定されています。
フォームに配置済みのコントロールを更新する
例えば仕様変更があって、下記のように設定ファイルを修正した場合ですね。
X,Y座標,項目名Eng,Jpn,MaxLength
10,80,field02,項目2だったよ,100
10,140,field04,項目4よん,30
410,50,field06,項目6どすぇ,30
410,140,field09,項目9ぅ,30
10,200,field11,項目11だっ,100
ソースの方は、
//設定ファイル読み込み
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");
}
}
...な感じに修正して、設定の追加・更新に対応しました。
コンパイルし直して実行すると、変更が反映されています!
例えば"項目2だったよ"のMaxLengthも100に変更されました。
コードの修正点は、searchForm内部関数で生成済みのLabel,TextBoxを探して、
- 見つかった場合はインスタンスを更新
- なければ生成&Containerに追加
...です。
ポイントは、生成済みのコントロールの場合はプロパティの変更が直ちに反映されるという所。
なので例えば自作コントロールの場合は、FindForm()で親フォームから配下のコントロールを弄る事ができたりします♪
まとめ
という事で、
- コントロールを追加する
- コントロールを追加・更新する
...の2パターンを作成しました。
弊社では今の所"コントロールを更新する"部分(追加はしない)で活用しています。
"実際にどのように配置するか?"とか、"どのような項目・コントロールを対象とするか?"は個々の事情により異なると思いますが、C#で操作できるという部分で自由度はかなり高いです!
大量に似たような画面を作成・メンテナンスするシチュエーションで活躍するかと思います(マスメン画面とか...)。
少しでも、どなたかの参考になれば幸いです(^^)
余談
"FilePathプロパティを作る代わりにOpenFileDialogを使っても良いです"とか書きそうになって、事前に試してみたらVisual Studio 2022が固まりました(^^;
なので100%何でも動かせるという訳ではありませんが、コントロールを弄ろうという範疇なら問題なく遊べるのではないかと思います!