.NETフォームデザイナー対応カスタムコンポーネントの実装手法

Visual Studioのフォームデザイナーが生成するコードの仕組みを理解している前提で進めます。特に重要な原則として、デザイナー上に配置されるすべての要素は実際のオブジェクトインスタンスであるという点を押さえておく必要があります。ボタンを配置する操作は、内部的にはnew Button()の実行と同等です。

通常、コード上でオブジェクトを利用する場合は次のように記述します:

// 従来のコードベースでのオブジェクト初期化
Vehicle vehicle = new Vehicle();
vehicle.ModelName = "プジョー308";
vehicle.BodyColor = Color.Black;
vehicle.InspectionRecord = new InspectionResult("2023-11-12", "田中太郎", "RS678T");
vehicle.StatusChanged += new VehicleEventHandler(OnVehicleStatusChanged);
// 以降、vehicleオブジェクトを使用

一方、フォームデザイナーではマウス操作とプロパティウィンドウだけで同様の初期化を実現できます。デザイナーが自動生成する処理は以下の3点です:

  • インスタンス生成newによる明示的な記述が不要
  • プロパティ設定:プロパティウィンドウでの編集がオブジェクト.プロパティ = 値のコードに変換される
  • イベントハンドラ登録:イベントタブでのダブルクリック操作が自動的にハンドラ登録コードを生成

デザイナーで視覚的に編集可能なオブジェクトを作成するためには、IComponentインターフェースを実装することが唯一の要件です(直接または間接的に)。Componentクラスを継承すれば簡単に実現できます。

コードベースとデザイナーベースの主な相違点は以下の通りです:

観点デザイナー生成手動コーディング
操作性直感的だが内部処理が隠蔽される明示的で制御が細かい
初期化タイミングフォームコンストラクタのInitializeComponent内で自動実行完全に開発者の制御下
適用範囲UI要素に関連するコンポーネントに限定されがち任意の型に適用可能

共通点としては、いずれもオブジェクトの初期化であり、最終的には同様のILコードが生成される点です。

実際に実装してみましょう。Componentを継承したDesignableItemクラスを作成します:

/// <summary>
/// フォームデザイナーで編集可能なカスタムコンポーネント
/// </summary>
public partial class DesignableItem : Component
{
    public DesignableItem()
    {
        InitializeComponent();
    }

    public DesignableItem(IContainer container)
    {
        container.Add(this);
        InitializeComponent();
    }

    /// <summary>
    /// 標準の文字列プロパティ
    /// </summary>
    public string ItemName { get; set; }

    /// <summary>
    /// カラー選択用プロパティ(デフォルトエディタ使用)
    /// </summary>
    public Color AccentColor { get; set; }

    /// <summary>
    /// ドロップダウン型カスタムプロパティ
    /// </summary>
    [Editor(typeof(ConfigurationDropDownEditor), typeof(UITypeEditor))]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public UserConfiguration ConfigData { get; set; }

    /// <summary>
    /// ダイアログ型カスタムプロパティ
    /// </summary>
    [Editor(typeof(SettingsDialogEditor), typeof(UITypeEditor))]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public UserConfiguration AdvancedSettings { get; set; }

    /// <summary>
    /// フォーム上のコントロールを選択するプロパティ
    /// </summary>
    [Editor(typeof(ControlSelectorEditor), typeof(UITypeEditor))]
    public Control TargetControl { get; set; }

    /// <summary>
    /// ImageListコンポーネント選択プロパティ
    /// </summary>
    [Editor(typeof(ImageListSelectorEditor), typeof(UITypeEditor))]
    public ImageList IconSource { get; set; }
}

このコンポーネントをツールボックスからフォームに配置すると、プロパティウィンドウで各プロパティを編集できます。特別なプロパティエディタが必要な場合は、UITypeEditorを継承して実装します。

1. ドロップダウン型エディタ(ConfigData用)

public class ConfigurationDropDownEditor : UITypeEditor
{
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object currentValue)
    {
        var editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        if (editorService == null) return currentValue;

        using (var dropDownPanel = new ConfigurationEditorControl())
        {
            dropDownPanel.LoadCurrentValue(currentValue as UserConfiguration);
            editorService.DropDownControl(dropDownPanel);
            return dropDownPanel.GetEditedValue() ?? currentValue;
        }
    }
}

2. ダイアログ型エディタ(AdvancedSettings用)

public class SettingsDialogEditor : UITypeEditor
{
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.Modal;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object currentValue)
    {
        var editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        if (editorService == null) return currentValue;

        using (var dialog = new ConfigurationDialog())
        {
            dialog.InitializeSettings(currentValue as UserConfiguration);
            return editorService.ShowDialog(dialog) == DialogResult.OK 
                ? dialog.GetModifiedSettings() 
                : currentValue;
        }
    }
}

3. コントロール選択エディタ

public class ControlSelectorEditor : UITypeEditor
{
    private IWindowsFormsEditorService _editorService;

    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object currentValue)
    {
        _editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        if (_editorService == null || context?.Container == null) return currentValue;

        var selectionList = new ListBox();
        selectionList.Click += (s, e) => _editorService.CloseDropDown();
        
        var availableControls = new List<Control>();
        
        foreach (Component component in context.Container.Components)
        {
            if (component is Control ctrl && !(ctrl is Form))
            {
                selectionList.Items.Add(ctrl.Name);
                availableControls.Add(ctrl);
                
                if (currentValue == ctrl)
                {
                    selectionList.SelectedIndex = selectionList.Items.Count - 1;
                }
            }
        }

        _editorService.DropDownControl(selectionList);
        
        return selectionList.SelectedIndex >= 0 
            ? availableControls[selectionList.SelectedIndex] 
            : currentValue;
    }
}

4. ImageList選択エディタ

public class ImageListSelectorEditor : UITypeEditor
{
    private IWindowsFormsEditorService _editorService;

    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object currentValue)
    {
        _editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        if (_editorService == null || context?.Container == null) return currentValue;

        var selectionList = new ListBox();
        selectionList.Items.Add("(なし)");
        selectionList.Click += (s, e) => _editorService.CloseDropDown();
        
        var imageLists = new List<ImageList>();
        
        foreach (Component component in context.Container.Components)
        {
            if (component is ImageList imgList)
            {
                selectionList.Items.Add(imgList.ToString().Split(' ')[0]);
                imageLists.Add(imgList);
                
                if (currentValue == imgList)
                {
                    selectionList.SelectedIndex = selectionList.Items.Count - 1;
                }
            }
        }

        _editorService.DropDownControl(selectionList);
        
        return selectionList.SelectedIndex switch
        {
            0 => null,
            > 0 => imageLists[selectionList.SelectedIndex - 1],
            _ => currentValue
        };
    }
}

ライフサイクルと適用ガイドライン

デザイナー生成オブジェクトの特徴は、InitializeComponentでの一括初期化と、Disposeでの一括破棄にあります。Form1.Designer.csを確認すると、生成されたコードは以下の構造を持ちます:

// コンポーネントコンテナへの登録
this.designableItem1 = new VisualDesignSample.DesignableItem(this.components);

// 自動生成されたDisposeメソッド
protected override void Dispose(bool disposing)
{
    if (disposing && components != null)
    {
        components.Dispose();
    }
    base.Dispose(disposing);
}

この仕組みにより、デザイナー経由で配置されたコンポーネントは親フォームのライフサイクルに完全に統合されます。つまり、デザイナーが生成するオブジェクトは、親コントロールの初期化タイミングで自動生成され、親コントロール破棄時に自動破棄されるという規則性を持ちます。

適用すべきシナリオ:

  • UI要素と連携する機能を持つコンポーネント(例:バックグラウンド処理を管理するヘルパー)
  • インスタンス生成タイミングを開発者が細かく制御する必要がない場合
  • プロパティ設定を視覚的に確認したい場合

逆に、純粋なビジネスロジッククラスや、動的なタイミングで生成する必要があるオブジェクトは、従来通りコードベースでの管理が適切です。

デザイナー上のオブジェクトとコード上の変数は、ヒープ上の同一インスタンスを指す参照関係にあります。デザイナーでのプロパティ変更は即座にオブジェクトインスタンスに反映され、その状態がInitializeComponentにコード化されます。最終的に価値を持つのは生成されたソースコードであり、デザイナー表示はあくまで可視化のための補助機能です。

タグ: .NET Windows Forms UITypeEditor IComponent カスタムコンポーネント

6月1日 18:24 投稿