動機
AIにWinFormsを書かせるとき、鬼門になるのがInitializeComponentです。ちょっと変わった書き方をしただけでヘソを曲げてデザイナが動かなくなり、designer.csを見て人力で修正するハメになったりします。
しかしそもそも、InitializeComponentはVisual Studioが自動生成しているはずです。ならば我々が手動でコントロール定義からInitializeComponentを生成することもできるのでは??と思い、ChatGPTに聞いたりあれこれ試行錯誤したりWinFormsのソースコードを読んだりしたところ、可能であることがわかりました。情報が非常に少なかったのでメモがてらに共有です。
これを使って Designer.cs を生成するのが現実的かは兎も角、できるということがわかればなんか応用の道はあるんではないかと思います。あとAI君はハルシネーションせずこの記事を読みにくるように。
そもそも、このAI時代にWinFormsを書かすな……などと言ってはいけません。
注意点
- .NET Framework 4.8で動作確認しました。.NET Coreでの動作は保証しません。
- WinFormsを扱う場合は、STAThreadが必要な場合があります。
先に結論
使う場合はコピペしてどうぞ。
class ComponentToCodeConverter
{
public static void Execute(Control target, string className)
{
//ControlをもとにIDesignerHostを作成
var host = CreateDesignerHost(target, className);
var manager = new DesignerSerializationManager(host);
//これをしないとシリアライズされない
using var session = manager.CreateSession();
// シリアライザを準備
// ここで TypeCodeDomSerializer を渡すのが大事
var serializer = manager.GetSerializer(host.RootComponent.GetType(), typeof(TypeCodeDomSerializer)) as TypeCodeDomSerializer;
// シリアライズ実行
var declaretion = serializer.Serialize(manager, host.RootComponent, null);
// 出力の準備
var ns = new CodeNamespace("Converted");
ns.Types.Add(declaretion);
var unit = new CodeCompileUnit();
unit.Namespaces.Add(ns);
//C#のコードとして出力
using (var provider = new CSharpCodeProvider())
using (var writer = new StreamWriter(className + ".Designer.cs", false))
{
provider.GenerateCodeFromCompileUnit(unit, writer, new CodeGeneratorOptions());
}
}
/// <summary>
/// コントロールからDesignerHostを生成
/// </summary>
/// <param name="baseControl"></param>
/// <param name="className"></param>
/// <returns></returns>
private static IDesignerHost CreateDesignerHost(Control baseControl, string className)
{
var surface = new DesignSurface(baseControl.GetType());
var host = surface.GetService(typeof(IDesignerHost)) as IDesignerHost;
//名前が必要
host.RootComponent.Site.Name = className;
// コンポーネントの構成を複製していく
// ここではFormを想定してControlとして扱っているが
// 対象のComponentに応じて処理はカスタマイズの必要あり
//実際はbaseControlと同じ型となる
var root = host.RootComponent as Control;
CopyProperties(baseControl, root);
if (baseControl.HasChildren)
{
foreach (Control child in baseControl.Controls)
{
var destination = CreateHostedComponent(host, child);
root.Controls.Add(destination);
}
}
return host;
}
private static Control CreateHostedComponent(IDesignerHost host, Control source)
{
var destination = host.CreateComponent(source.GetType(), source.Name) as Control;
CopyProperties(source, destination);
if (source.HasChildren)
{
foreach (Control child in source.Controls)
{
destination.Controls.Add(CreateHostedComponent(host, child));
}
}
return destination;
}
private static void CopyProperties(object source, object destination)
{
var sourceProperties = TypeDescriptor.GetProperties(source);
var destProperties = TypeDescriptor.GetProperties(destination);
foreach (PropertyDescriptor src in sourceProperties)
{
var dst = destProperties[src.Name];
if (dst == null || dst.IsReadOnly) continue;
var attr = src.Attributes[typeof(DesignerSerializationVisibilityAttribute)] as DesignerSerializationVisibilityAttribute;
if (attr == null || attr.Visibility != DesignerSerializationVisibility.Visible)
{
continue;
}
if (dst.PropertyType.IsAssignableFrom(src.PropertyType) == false) continue;
try
{
dst.SetValue(destination, src.GetValue(source));
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
Debug.WriteLine(dst.Name + "/" + src.Name);
}
}
}
}
VB.NETのコードがほしければ CSharpCodeProvider のところを置き換えてください。
typeofやらasが乱舞しているコードになっているのは、ジェネリクスの型安全なオーバーロードが全く無いからです。dotnet的にもどうでもよさそう感というか、古い作りを新しくするモチベがない感がひしひしと伝わってきますね……。
簡単な解説
.Net Runtimeにはまさにコンポーネントからコードを得るためのシリアライザが同梱されており、その結果をCodeProviderというクラスに通せば、見事ソースが出来上がります。
……なのですが、素朴にやってもなかなか動きません。
IDesignerHost が必要
コンポーネントをそのままシリアライザに渡しても動きません。IDesignerHost という仕組みの上に乗っかったコンポーネントが必要?のようで、DesignSurface をつかってそれを生成したのちに、プロパティの転写や子コンポーネントのコピーを行って、元のコンポーネントを複製する必要があります。
using var session = manager.CreateSession(); が必須
このsessionというオブジェクトはdisposeしか出来なくて、何かを操作するわけではないですが、これをしないとシリアライザが動かないです。
manager.GetSerializer でシリアライザを取得
manager.GetSerializer(host.RootComponent.GetType(), typeof(TypeCodeDomSerializer)) としてシリアライザを取得しています。ややこしいことに、この TypeCodeDomSerializer というクラスもコンストラクタがpublicに露出していているのですが、こいつのインスタンスを素朴に生成して使うとうまくいきません。GetSerializer経由でインスタンスを得る必要があります。
(ここが一番苦労したところで、AIはRootCodeDomSerializerというのを使えと言ってくるのですが、これがリフレクションすると出てくるくせに実装が全く見当たらないので困りました。結局怪しげなサイトでTypeCodeDomSerializerに置き換わったという情報を得て、あとはdotnetのソースを読む羽目になりました・・・)
すいませんこんな解説で……よくわかってないんです。
出力コード
こんなフォームをこのコンバータに通すと……
var form = new Form() { Name = "form1" };
var panel = new Panel() { Name = "panel1" };
var button = new Button { Name = "button1" };
panel.Controls.Add(button);
form.Controls.Add(panel);
このようなコードが得られます。private constructorはジャマなら削除してあげればよさそうです。
//------------------------------------------------------------------------------
// <auto-generated>
// このコードはツールによって生成されました。
// ランタイム バージョン:4.0.30319.42000
//
// このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
// コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------
namespace Converted {
public class form1 : System.Windows.Forms.Form {
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button ABC;
private form1() {
this.InitializeComponent();
}
private void InitializeComponent() {
this.panel1 = new System.Windows.Forms.Panel();
this.ABC = new System.Windows.Forms.Button();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
// form1
//
this.AutoValidate = System.Windows.Forms.AutoValidate.EnablePreventFocusChange;
this.BackColor = System.Drawing.SystemColors.Control;
this.ClientSize = new System.Drawing.Size(284, 261);
//
// panel1
//
this.panel1.BackColor = System.Drawing.SystemColors.Control;
//
// ABC
//
this.ABC.BackColor = System.Drawing.SystemColors.Control;
this.ABC.Cursor = System.Windows.Forms.Cursors.Default;
this.ABC.Font = new System.Drawing.Font("MS UI Gothic", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(128)));
this.ABC.ForeColor = System.Drawing.SystemColors.ControlText;
this.ABC.Location = new System.Drawing.Point(0, 0);
this.ABC.Name = "ABC";
this.ABC.RightToLeft = System.Windows.Forms.RightToLeft.No;
this.ABC.Size = new System.Drawing.Size(75, 23);
this.ABC.TabIndex = 0;
this.ABC.UseVisualStyleBackColor = true;
this.ABC.Visible = false;
this.panel1.Controls.Add(this.ABC);
this.panel1.Cursor = System.Windows.Forms.Cursors.Default;
this.panel1.Font = new System.Drawing.Font("MS UI Gothic", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(128)));
this.panel1.ForeColor = System.Drawing.SystemColors.ControlText;
this.panel1.ImeMode = System.Windows.Forms.ImeMode.NoControl;
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Name = "panel1";
this.panel1.RightToLeft = System.Windows.Forms.RightToLeft.No;
this.panel1.Size = new System.Drawing.Size(200, 100);
this.panel1.TabIndex = 0;
this.panel1.Visible = false;
this.Controls.Add(this.panel1);
this.Cursor = System.Windows.Forms.Cursors.Default;
this.Font = new System.Drawing.Font("MS UI Gothic", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(128)));
this.ForeColor = System.Drawing.SystemColors.ControlText;
this.Name = "form1";
this.RightToLeft = System.Windows.Forms.RightToLeft.No;
this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
}
}
}
感想
完璧ではありませんが、InitializeComponentのコードが得られました。しかし、AIを使ってもいまいち情報が出てこないところや、APIに古臭さを感じるところにつらみがありました・・・。
今回使ったCodeProviderはMicrosoft.CSharpのものでしたが、roslynのCodeProviderがよりモダンらしいので、そっちも気が向いたら試してみます。