「コントロール - カスタムコントロール(C Sharp)」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

文字列「<source」を「<syntaxhighlight」に置換
編集の要約なし
 
1行目: 1行目:
== 概要 ==
== 概要 ==
Windowsフォームに於いて、標準のコントロールを拡張したカスタムコントロールをDLLとして作成する方法を記述する。<br>
Windows Formsに於いて、標準のコントロールだけでは要件を満たせない場合、カスタムコントロールを作成して独自のUI要素を実現できる。<br>
<br>
カスタムコントロールは大きく3つのアプローチに分類される。<br>
<br>
* User Control (System.Windows.Forms.UserControl継承)
*: 複数の既存コントロールを組み合わせて単一の再利用可能なコンポーネントとする。
*: 例: 顧客情報入力パネル (DataGridView + BindingSource + BindingNavigator)
*: 既存コントロールの機能をそのまま活用でき、実装が容易である。
*: <br>
* Extended Control (既存コントロール継承: Button, TextBox等)
*: 既存コントロールの機能を保持しつつ、カスタム機能の追加や外観の変更を行う。
*: 例: クリック時にテキストを全選択するテキストボックス
*: OnPaintオーバーライドで外観もカスタマイズ可能である。
*: <br>
* Custom Control (System.Windows.Forms.Control継承)
*: 完全にカスタムな描画と動作を実装する。
*: 例: アナログ時計コントロール
*: 最大の柔軟性を持つが、実装コストが大きい。
<br>
Windows Formsコントロールの継承階層は、System.Windows.Forms.Control (基底クラス: ウィンドウハンドル管理、メッセージルーティング、マウス / キーボードイベント) を頂点として、<br>
各種コントロール (Button, TextBox, ListBox等) と UserControl (複合コントロール向け) に分岐する構造となっている。<br>
<br><br>
<br><br>


9行目: 29行目:
ソリューションとプロジェクトが作成されるが、プロジェクトに初期配置されているClass1.csファイルは不要なので削除する。<br>
ソリューションとプロジェクトが作成されるが、プロジェクトに初期配置されているClass1.csファイルは不要なので削除する。<br>
<br>
<br>
<プロジェクト名>の上で右クリックして、[追加] - [新しい項目] - [カスタムコントロール]を選択する。<br>
<プロジェクト名> の上で右クリックして、[追加] - [新しい項目] - [カスタムコントロール]を選択する。<br>
(右上の検索バーを使用すると簡単に見つけることができる)<br>
(右上の検索バーを使用すると簡単に見つけることができる)<br>
<br><br>
<br><br>
18行目: 38行目:
<br>
<br>
まず、カスタムコントロールのデザイン画面でイベントを作成する。(プロパティのイベントの[Click]項目を選択する)<br>
まず、カスタムコントロールのデザイン画面でイベントを作成する。(プロパティのイベントの[Click]項目を選択する)<br>
作成されたイベントメソッドに、this.SelectAll();と記述して、クリック時にテキストを全選択できるようにする。<br>
作成されたイベントメソッドに、<code>this.SelectAll();</code> と記述して、クリック時にテキストを全選択できるようにする。<br>
<br>
<br>
以下のように、CustomControl1.csのソースコードを記述する。<br>
以下に示すように、CustomControl1.csのソースコードを記述する。<br>
ビルドすることでDLLが作成される。<br>
ビルドすることでDLLが作成される。<br>
<br>
  <syntaxhighlight lang="c#">
  <syntaxhighlight lang="c#">
  using System;
  using System;
57行目: 78行目:
<br><br>
<br><br>


== カスタムコントロール(DLL)の使用方法 ==
== カスタムコントロール (DLL) の使用方法 ==
上記で作成したカスタムコンロトール(DLL)を他プロジェクトで使用する。<br>
上記で作成したカスタムコンロトール (DLL) を他プロジェクトで使用する。<br>
<br>
<br>
まず、他ソリューションとして、Windowsフォームプロジェクトを作成する。<br>
まず、他ソリューションとして、Windowsフォームプロジェクトを作成する。<br>
<br>
<br>
次に、[メニューバー] - [ツール] -[ツールボックスアイテムの選択]を選択して、作成したカスタムコントロールをツールボックスに表示させる。<br>
次に、[メニューバー] - [ツール] - [ツールボックスアイテムの選択]を選択して、作成したカスタムコントロールをツールボックスに表示させる。<br>
[ツールボックスアイテムの選択]画面が表示されるので、[参照]ボタンを押下して、上記で作成したDLLを選択する。<br>
[ツールボックスアイテムの選択]画面が表示されるので、[参照]ボタンを押下して、上記で作成したDLLを選択する。<br>
<br>
<br>
.NET Frameworkコンポーネントにカスタムコントロールが追加されたことを確認して、[OK]ボタンを押下する。<br>
.NET / .NET Frameworkコンポーネントにカスタムコントロールが追加されたことを確認して、[OK]ボタンを押下する。<br>
[ツールボックス]にカスタムコントロールが配置されるので確認する。<br>
[ツールボックス]にカスタムコントロールが配置されるので確認する。<br>
<br>
<br>
71行目: 92行目:
カスタムコントロールに文字列を入力して、クリックすると全選択されるか確認する。<br>
カスタムコントロールに文字列を入力して、クリックすると全選択されるか確認する。<br>
<br><br>
<br><br>
== カスタムコントロールの描画 ==
カスタムコントロールの外観は、OnPaintメソッドをオーバーライドすることで完全に制御できる。<br>
描画処理を適切に実装することにより、標準コントロールには存在しない独自の視覚的表現を実現できる。<br>
<br>
==== OnPaintメソッドのオーバーライド ====
<code>OnPaint</code> メソッドをオーバーライドすることで、コントロールの描画処理をカスタマイズできる。<br>
<br>
引数の <code>PaintEventArgs</code> には、以下に示す情報が含まれる。<br>
<br>
* Graphics
*: 描画オブジェクト
*: 線、図形、テキスト等の描画に使用する。
* ClipRectangle
*: 再描画が必要な領域を示す矩形
*: この領域のみを再描画することで効率化できる。
<br>
<code>ClientRectangle</code> はコントロール全体の利用可能な領域を示し、<code>ClipRectangle</code> と異なり常にコントロール全体のサイズを返す。<br>
<br>
以下の例では、コントロールの境界に矩形を描画している。<br>
<br>
<syntaxhighlight lang="c#">
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    using var myPen = new Pen(Color.Aqua);
    var area = new Rectangle(new Point(0, 0), new Size(this.Size.Width - 1, this.Size.Height - 1));
    e.Graphics.DrawRectangle(myPen, area);
}
</syntaxhighlight>
<br>
==== OnPaintBackgroundメソッド ====
<code>OnPaintBackground</code> メソッドをオーバーライドすることにより、背景描画を前景描画から分離できる。<br>
背景の描画を独立させることで、コードの見通しが良くなり、ダブルバッファリングとの親和性も高まる。<br>
<br>
以下の例では、グラデーション背景を描画している。<br>
<br>
<syntaxhighlight lang="c#">
protected override void OnPaintBackground(PaintEventArgs e)
{
    using var brush = new LinearGradientBrush(
      new Point(0, 0),
      new Point(0, this.Height),
      Color.White,
      Color.LightBlue);
    e.Graphics.FillRectangle(brush, this.ClientRectangle);
}
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    // 前景の詳細描画...
}
</syntaxhighlight>
<br>
==== ダブルバッファリング ====
ダブルバッファリングは、描画のちらつきを防止するための技術である。<br>
<br>
描画をオフスクリーンバッファで行い、完成後に一括して画面に転送することで、描画途中の状態が表示されるのを防ぐ。<br>
<br>
実装方法は3つある。<br>
<br>
* 方法 1
*: DoubleBufferedプロパティ (推奨)
*: 最も簡単な方法であり、コンストラクタで有効化するだけでよい。
*: <syntaxhighlight lang="c#">
public MyCustomControl()
{
    InitializeComponent();
    this.DoubleBuffered = true;
}
</syntaxhighlight>
*: <br>
* 方法 2
*: SetStyleメソッド
*: 描画スタイルを細かく制御したい場合に使用する。
*: <syntaxhighlight lang="c#">
public MyCustomControl()
{
    InitializeComponent();
    SetStyle(ControlStyles.UserPaint, true);
    SetStyle(ControlStyles.AllPaintingInWmPaint, true);
    SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
</syntaxhighlight>
*: <br>
* 方法 3
*: BufferedGraphicsContext (手動制御)
*: 描画タイミングを完全に手動制御したい場合に使用する。
*: <syntaxhighlight lang="c#">
private BufferedGraphicsContext context = BufferedGraphicsManager.Current;
protected override void OnPaint(PaintEventArgs e)
{
    var bufferedGraphics = context.Allocate(e.Graphics, this.ClientRectangle);
    bufferedGraphics.Graphics.Clear(this.BackColor);
    // 描画処理...
    bufferedGraphics.Render();
    bufferedGraphics.Dispose();
}
</syntaxhighlight>
<br>
==== InvalidateとUpdateの使い分け ====
コントロールの再描画を制御するメソッドは3種類ある。<br>
<br>
* Invalidate()
*: 指定した領域を「再描画が必要」とマークする。(非同期)
*: OnPaintは即座には呼び出されない。
* Update()
*: マークされた領域を即座に同期再描画する。
* Refresh()
*: Invalidate(true) + Update() の短縮形
*: コントロール全体を即座に再描画する。
<br>
以下に代表的な使用パターンを示す。<br>
<br>
<syntaxhighlight lang="c#">
// パターン1: 特定領域の即座な更新
var dirtyRect = new Rectangle(10, 10, 50, 50);
this.Invalidate(dirtyRect);
this.Update();
// パターン2: 複数の変更をまとめて反映
this.someProperty = newValue1;
this.anotherProperty = newValue2;
this.Invalidate();
this.Update();
// パターン3: コントロール全体の更新
this.Refresh();
</syntaxhighlight>
<br><br>
== カスタムプロパティの追加 ==
カスタムコントロールには、Visual Studioのプロパティウィンドウで設定可能なカスタムプロパティを追加できる。<br>
<br>
プロパティに各種デザイナー属性を付与することにより、プロパティウィンドウへの表示方法を制御できる。<br>
<br>
以下の例では、カスタムプロパティを定義している。<br>
<br>
<syntaxhighlight lang="c#">
public class MyCustomControl : Control
{
    private int angle = 0;
    private string placeholderText = "Enter text...";
    private Color cornerColor = Color.Blue;
    [Browsable(true)]
    [Category("Appearance")]
    [Description("コントロールの角度 (度単位)")]
    [DefaultValue(0)]
    public int Angle
    {
      get => angle;
      set
      {
          if (angle != value)
          {
            angle = value;
            Invalidate();
          }
      }
    }
    [Browsable(false)]
    public string InternalState { get; set; }
    [Browsable(true)]
    [Category("Appearance")]
    [Description("テキストが空の時に表示するプレースホルダーテキスト")]
    [DefaultValue("Enter text...")]
    public string PlaceholderText
    {
      get => placeholderText;
      set
      {
          if (placeholderText != value)
          {
            placeholderText = value;
            Invalidate();
          }
      }
    }
    [Browsable(true)]
    [Category("Appearance")]
    [Description("コーナー部分の描画色")]
    [DefaultValue(typeof(Color), "Blue")]
    public Color CornerColor
    {
      get => cornerColor;
      set
      {
          if (cornerColor != value)
          {
            cornerColor = value;
            Invalidate();
          }
      }
    }
}
</syntaxhighlight>
<br>
下表に、主なデザイナー属性を示す。<br>
<br>
<center>
{| class="wikitable"
|+ デザイナー属性一覧
! 属性 !! 用途
|-
| <code>[Browsable(bool)]</code> || プロパティウィンドウに表示するか制御する。
|-
| <code>[Category(string)]</code> || プロパティウィンドウ内のカテゴリ名を設定する。
|-
| <code>[Description(string)]</code> || プロパティウィンドウ下部に表示される説明文を設定する。
|-
| <code>[DefaultValue(object)]</code> || デフォルト値を指定する。(デザイナーシリアライゼーション基準値)
|-
| <code>[ReadOnly(bool)]</code> || プロパティが読み取り専用かを設定する。
|-
| <code>[DesignerSerializationVisibility]</code> || デザイナーシリアライゼーション動作を設定する。(Content / Hidden / Visible)
|}
</center>
<br>
==== TypeConverterの使用 ====
<code>TypeConverter</code> を使用することにより、プロパティウィンドウでのカスタム型の入力 / 変換方法を定義できる。<br>
<br>
以下の例では、文字列を <code>Size</code> 型に変換するカスタムコンバーターを定義している。<br>
<br>
<syntaxhighlight lang="c#">
public class SizeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
      return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }
    public override object ConvertFrom(ITypeDescriptorContext context,
      CultureInfo culture, object value)
    {
      if (value is string str)
      {
          var parts = str.Split(',');
          if (parts.Length == 2 &&
            int.TryParse(parts[0].Trim(), out var width) &&
            int.TryParse(parts[1].Trim(), out var height))
          {
            return new Size(width, height);
          }
      }
      return base.ConvertFrom(context, culture, value);
    }
}
[TypeConverter(typeof(SizeConverter))]
public Size CustomSize { get; set; }
</syntaxhighlight>
<br><br>
== カスタムイベントの定義 ==
カスタムコントロールでは、標準のイベントに加えて独自のイベントを定義し、フォーム側で処理を受け取ることができる。<br>
<br>
==== EventArgsの拡張 ====
イベントに付加情報を持たせる場合は、<code>EventArgs</code> を継承してカスタムクラスを作成する。<br>
<br>
以下の例では、スコア情報を持つイベント引数クラスを定義している。<br>
<br>
<syntaxhighlight lang="c#">
public class ScoreEventArgs : EventArgs
{
    public int Score { get; set; }
    public DateTime ScoreTime { get; set; }
    public string ScoreLevel { get; set; }
    public ScoreEventArgs(int score, string level)
    {
      Score = score;
      ScoreLevel = level;
      ScoreTime = DateTime.Now;
    }
}
public class ValueChangedEventArgs : EventArgs
{
    public object OldValue { get; set; }
    public object NewValue { get; set; }
}
</syntaxhighlight>
<br>
==== イベントの定義と発火 ====
カスタムイベントは <code>EventHandler<T></code> を使用して定義する。<br>
<br>
イベントの発火は <code>protected virtual OnXxx</code> パターンで実装することにより、サブクラスでのオーバーライドを可能にする。<br>
<br>
<syntaxhighlight lang="c#">
public class MyCustomControl : Control
{
    public event EventHandler<ScoreEventArgs> ScoreChanged;
    public event EventHandler CornerClicked;
    protected virtual void OnScoreChanged(ScoreEventArgs e)
    {
      ScoreChanged?.Invoke(this, e);
    }
    protected virtual void OnCornerClicked(EventArgs e)
    {
      CornerClicked?.Invoke(this, e);
    }
    private int score = 0;
    public int Score
    {
      get => score;
      set
      {
          if (score != value)
          {
            score = value;
            var args = new ScoreEventArgs(score, GetScoreLevel(score));
            OnScoreChanged(args);
          }
      }
    }
    protected override void OnClick(EventArgs e)
    {
      base.OnClick(e);
      var mousePos = this.PointToClient(MousePosition);
      if (IsCornerArea(mousePos))
      {
          OnCornerClicked(e);
      }
    }
    private bool IsCornerArea(Point p)
    {
      const int cornerSize = 20;
      return (p.X < cornerSize && p.Y < cornerSize) ||
              (p.X > this.Width - cornerSize && p.Y < cornerSize) ||
              (p.X < cornerSize && p.Y > this.Height - cornerSize) ||
              (p.X > this.Width - cornerSize && p.Y > this.Height - cornerSize);
    }
    private string GetScoreLevel(int score)
    {
      return score switch
      {
          >= 90 => "Excellent",
          >= 70 => "Good",
          >= 50 => "Fair",
          _ => "Low"
      };
    }
}
</syntaxhighlight>
<br>
==== イベントの使用例 ====
フォーム側でカスタムイベントのハンドラを登録する方法を示す。<br>
<br>
<syntaxhighlight lang="c#">
public partial class MainForm : Form
{
    public MainForm()
    {
      InitializeComponent();
      var customControl = new MyCustomControl();
      customControl.ScoreChanged += CustomControl_ScoreChanged;
      customControl.CornerClicked += CustomControl_CornerClicked;
      this.Controls.Add(customControl);
    }
    private void CustomControl_ScoreChanged(object sender, ScoreEventArgs e)
    {
      MessageBox.Show($"Score changed to {e.Score} ({e.ScoreLevel}) at {e.ScoreTime:HH:mm:ss}");
    }
    private void CustomControl_CornerClicked(object sender, EventArgs e)
    {
      MessageBox.Show("You clicked a corner!");
    }
}
</syntaxhighlight>
<br><br>
== デザイナーサポート ==
Visual Studioのデザイナーと連携することで、カスタムコントロールを設計時に視覚的に配置・設定できる。<br>
<br>
デザイナーサポートを適切に実装することにより、コントロールの利便性が大幅に向上する。<br>
<br>
==== DesignModeプロパティ ====
<code>DesignMode</code> プロパティを使用することで、設計時と実行時で異なる描画 / 動作を実装できる。<br>
<br>
設計時はプレースホルダーを表示し、実行時はデータを表示するといった使い分けが可能である。<br>
<br>
<u>※注意</u><br>
<u><code>DesignMode</code> プロパティはコンストラクタ内では使用できない。</u><br>
<u>サイト割り当て前のため、常に <code>false</code> を返す。</u><br>
<br>
<syntaxhighlight lang="c#">
public class MyCustomControl : Control
{
    protected override void OnPaint(PaintEventArgs e)
    {
      base.OnPaint(e);
      if (this.DesignMode)
      {
          e.Graphics.DrawString("MyCustomControl",
            this.Font, Brushes.Gray, new PointF(5, 5));
      }
      else
      {
          // 実行時の描画処理
      }
    }
}
</syntaxhighlight>
<br>
==== DesignerSerializationVisibility属性 ====
<code>DesignerSerializationVisibility</code> 属性を使用して、デザイナーがプロパティをどのようにシリアライズするかを制御できる。<br>
<br>
* Content
*: コレクション等のプロパティの内容をシリアライズする。
* Visible
*: プロパティの値をシリアライズする。(デフォルト動作)
* Hidden
*: プロパティをシリアライズしない。(実行時のみのプロパティに使用する)
<br>
<syntaxhighlight lang="c#">
public class MyCustomControl : Control
{
    private List<string> items = new List<string>();
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public List<string> Items { get => items; }
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public int CustomValue { get; set; }
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public string RuntimeOnlyValue { get; set; }
}
</syntaxhighlight>
<br>
==== カスタムデザイナー ====
<code>Designer</code> 属性と <code>ControlDesigner</code> を継承することにより、Visual Studioデザイナーの動作を詳細にカスタマイズできる。<br>
<br>
<syntaxhighlight lang="c#">
[Designer(typeof(MyCustomControlDesigner))]
public class MyCustomControl : Control
{
    // コントロール実装
}
public class MyCustomControlDesigner : ControlDesigner
{
    public override void Initialize(IComponent component)
    {
      base.Initialize(component);
    }
  protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
    }
}
</syntaxhighlight>
<br><br>
== 高度なテクニック ==
==== WndProcのオーバーライド ====
<code>WndProc</code> をオーバーライドすることにより、Windowsメッセージを直接処理できる。<br>
<br>
<u>標準のイベントでは対応できない低レベルな操作が必要な場合に使用する。</u><br>
<br>
<u>※注意</u><br>
<u>処理しないメッセージは必ず <code>base.WndProc()</code> に転送しなければならない。</u><br>
<u>転送しないとコントロールの基本機能が破損する。</u><br>
<br>
<syntaxhighlight lang="c#">
public class MyCustomControl : Control
{
    private const int WM_KEYUP = 0x101;
    private const int WM_MOUSEMOVE = 0x200;
    private const int WM_LBUTTONDOWN = 0x201;
    private const int WM_PAINT = 0x0F;
    protected override void WndProc(ref Message m)
    {
      switch (m.Msg)
      {
          case WM_KEYUP:
            if ((int)m.WParam == (int)Keys.Escape)
            {
                MessageBox.Show("ESCキーが押されました");
            }
            break;
          case WM_MOUSEMOVE:
            int x = (int)m.LParam & 0xFFFF;
            int y = ((int)m.LParam >> 16) & 0xFFFF;
            break;
          case WM_LBUTTONDOWN:
            break;
          default:
            base.WndProc(ref m);
            break;
      }
    }
}
</syntaxhighlight>
<br>
==== オーナードロー ====
オーナードローを使用することで、ListBox等の標準コントロールの各項目を独自に描画できる。<br>
<br>
<code>DrawMode.OwnerDrawFixed</code> を設定し、<code>OnDrawItem</code> をオーバーライドして描画処理を実装する。<br>
<br>
以下の例では、カスタム描画を行うListBoxを定義している。<br>
<br>
<syntaxhighlight lang="c#">
public class MyCustomListBox : ListBox
{
    public MyCustomListBox()
    {
      this.DrawMode = DrawMode.OwnerDrawFixed;
      this.ItemHeight = 30;
    }
    protected override void OnDrawItem(DrawItemEventArgs e)
    {
      if (e.Index < 0) return;
      e.DrawBackground();
      string text = this.Items[e.Index].ToString();
      Color textColor = (e.State & DrawItemState.Selected) != 0 ? Color.White : Color.Black;
      using (var brush = new SolidBrush(textColor))
      {
          e.Graphics.DrawString(text, e.Font, brush, e.Bounds.Left + 5, e.Bounds.Top + 5);
      }
      e.DrawFocusRectangle();
    }
}
</syntaxhighlight>
<br>
==== 複合コントロール ====
<u>UserControl</u> をベースにして、複数の既存コントロールを1つのコンポーネントとして組み合わせることができる。<br>
<br>
以下の例では、住所入力パネルの複合コントロールを定義している。<br>
<br>
<syntaxhighlight lang="c#">
[Designer(typeof(ParentControlDesigner))]
public class AddressControl : UserControl
{
    private TextBox nameTextBox;
    private TextBox addressTextBox;
    private TextBox phoneTextBox;
    private Label nameLabel;
    private Label addressLabel;
    private Label phoneLabel;
    public AddressControl()
    {
      InitializeComponent();
    }
    private void InitializeComponent()
    {
      nameLabel = new Label { Text = "Name:" };
      nameTextBox = new TextBox();
      addressLabel = new Label { Text = "Address:" };
      addressTextBox = new TextBox { Multiline = true };
      phoneLabel = new Label { Text = "Phone:" };
      phoneTextBox = new TextBox();
      this.Controls.AddRange(new Control[]
      {
          nameLabel, nameTextBox,
          addressLabel, addressTextBox,
          phoneLabel, phoneTextBox
      });
    }
    [Browsable(true)]
    [Category("Data")]
    public string PersonName
    {
      get => nameTextBox.Text;
      set => nameTextBox.Text = value;
    }
    public string Address
    {
      get => addressTextBox.Text;
      set => addressTextBox.Text = value;
    }
    public string Phone
    {
      get => phoneTextBox.Text;
      set => phoneTextBox.Text = value;
    }
}
</syntaxhighlight>
<br><br>
== .NET 8以降のWindows Forms ==
.NET 8以降では、Windows FormsのAPIが大幅に改善されており、カスタムコントロール開発においても恩恵を受けられる。<br>
<br>
==== .NET 8 ====
新しいデータバインディングエンジンが導入された。<br>
<br>
<code>ICommand</code> インターフェースのサポートが追加され、<code>Button.Command</code> プロパティに <code>ICommand</code> を割り当てられるようになった。<br>
これによりMVVM設計パターンがWindows Formsでも実装しやすくなった。<br>
<br>
==== .NET 9 ====
以下に示す機能が追加または変更された。<br>
<br>
* 非同期フォーム機能
*: UI関連の操作を非同期で実行できるAPI群が追加された。
* ダークモード対応 (実験的)
*: アプリケーションがダークテーマに自動適応する機能
* BinaryFormatterの削除
*: セキュリティリスク排除のため完全に削除された。
* ToolStrip.AllowClickThrough
*: フォーム非フォーカス状態でToolStripを操作可能になった。
<br>
==== 移行時の注意点 ====
.NET Frameworkから.NET 8以降へ移行する場合は、以下に示す点に注意する。<br>
<br>
* フォームとカスタムコントロールのレイアウトを確認する。
*: フォント差異によるレイアウトへの影響が発生する場合がある。
*: <br>
* サードパーティコントロールの.NET 8以降の対応版を確認する
*: 互換性のないコントロールは代替を検討する必要がある。
*: <br>
* Microsoft.Windows.Compatibility NuGetパッケージで約20,000 APIを補完できる
*: .NET Frameworkにのみ存在するAPIの互換レイヤーを提供する。
*: <br>
* ターゲットフレームワークはWindows専用 (netX.0-windows) となる
*: クロスプラットフォーム実行はできない。
<br><br>
== NuGetパッケージとしての配布 ==
作成したカスタムコントロールライブラリをNuGetパッケージとして配布することにより、他のプロジェクトや開発者に共有できる。<br>
<br>
==== .csprojの設定 ====
<u>.csprojファイル</u> に必要なパッケージメタデータを設定する。<br>
<br>
<syntaxhighlight lang="xml">
<PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <IsToolboxControlLibrary>true</IsToolboxControlLibrary>
    <Title>My Custom Controls Library</Title>
    <Description>カスタムコントロールライブラリ説明</Description>
    <Authors>開発者名または組織名</Authors>
    <Version>1.0.0</Version>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/your/repo</PackageProjectUrl>
    <RepositoryUrl>https://github.com/your/repo</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
</syntaxhighlight>
<br>
==== デザイナーアセンブリのフォルダ構造 ====
Visual Studioのデザイナーでコントロールを使用可能にするためには、以下に示すフォルダ構造でDLLを配置する。<br>
<br>
<syntaxhighlight lang="bash">
lib/
    net8.0-windows/
      MyControls.dll
      MyControls.xml
      Design/
          WinForms/
            MyControls.Design.dll
            MyControls.Design.xml
</syntaxhighlight>
<br>
==== 配布方法 ====
パッケージの配布先に応じて、以下に示す方法を使用する。<br>
<br>
* NuGet.org (公開配布)
*: <code>dotnet nuget push MyControls.1.0.0.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json</code>
*: <br>
* 社内NuGetフィード (プライベート配布)
*: 組織内の専用フィードサーバを利用する。
*: <br>
* GitHub Packages
*: GitHubリポジトリと連携したパッケージ管理を利用する。
<br><br>
== 実践的な例 ==
==== プレースホルダー付きテキストボックス ====
テキストが空の場合にグレーのヒントテキストを表示するテキストボックスの例である。<br>
<br>
<syntaxhighlight lang="c#">
public class PlaceholderTextBox : TextBox
{
    private string placeholder = "Enter text...";
    private bool showPlaceholder = true;
    [Browsable(true)]
    [Category("Appearance")]
    [Description("プレースホルダーテキスト")]
    public string Placeholder
    {
      get => placeholder;
      set
      {
          placeholder = value;
          if (showPlaceholder)
            Invalidate();
      }
    }
    public PlaceholderTextBox()
    {
      this.Enter += (s, e) => { if (showPlaceholder) this.Text = ""; };
      this.Leave += (s, e) =>
      {
          if (string.IsNullOrEmpty(this.Text))
          {
            this.Text = placeholder;
            showPlaceholder = true;
          }
      };
    }
    protected override void OnPaint(PaintEventArgs e)
    {
      base.OnPaint(e);
      if (showPlaceholder && string.IsNullOrEmpty(this.Text))
      {
          using (var brush = new SolidBrush(Color.Gray))
          {
            e.Graphics.DrawString(placeholder, this.Font, brush, 3, 3);
          }
      }
    }
}
</syntaxhighlight>
<br>
==== カスタムボタン (角丸・グラデーション) ====
角丸とグラデーション塗りつぶしを持つカスタムボタンの例である。<br>
<br>
<syntaxhighlight lang="c#">
public class RoundGradientButton : Button
{
    private Color gradientColor1 = Color.SteelBlue;
    private Color gradientColor2 = Color.LightBlue;
    private int cornerRadius = 15;
    [Browsable(true)]
    [Category("Appearance")]
    public Color GradientColor1
    {
      get => gradientColor1;
      set { gradientColor1 = value; Invalidate(); }
    }
    [Browsable(true)]
    [Category("Appearance")]
    public Color GradientColor2
    {
      get => gradientColor2;
      set { gradientColor2 = value; Invalidate(); }
    }
    [Browsable(true)]
    [Category("Appearance")]
    [DefaultValue(15)]
    public int CornerRadius
    {
      get => cornerRadius;
      set { cornerRadius = value; Invalidate(); }
    }
    public RoundGradientButton()
    {
      this.FlatStyle = FlatStyle.Flat;
      this.FlatAppearance.BorderSize = 0;
      SetStyle(ControlStyles.ResizeRedraw, true);
    }
    protected override void OnPaint(PaintEventArgs e)
    {
      e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
      var path = GetRoundedRectanglePath(this.ClientRectangle, cornerRadius);
      using (var brush = new LinearGradientBrush(
          new Point(0, 0),
          new Point(0, this.Height),
          gradientColor1,
          gradientColor2))
      {
          e.Graphics.FillPath(brush, path);
      }
      using (var pen = new Pen(Color.DarkGray, 1))
      {
          e.Graphics.DrawPath(pen, path);
      }
      var textSize = e.Graphics.MeasureString(this.Text, this.Font);
      var textX = (this.Width - textSize.Width) / 2;
      var textY = (this.Height - textSize.Height) / 2;
      using (var brush = new SolidBrush(this.ForeColor))
      {
          e.Graphics.DrawString(this.Text, this.Font, brush, textX, textY);
      }
    }
    private GraphicsPath GetRoundedRectanglePath(Rectangle rect, int radius)
    {
      var path = new GraphicsPath();
      var d = radius * 2;
      path.AddArc(rect.X, rect.Y, d, d, 180, 90);
      path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
      path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
      path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
      path.CloseFigure();
      return path;
    }
}
</syntaxhighlight>
<br>
==== 数値入力専用テキストボックス ====
数値以外の入力を拒否し、入力値の検証も行うテキストボックスの例である。<br>
<br>
<syntaxhighlight lang="c#">
public class NumericTextBox : TextBox
{
    private bool allowDecimal = false;
    [Browsable(true)]
    [Category("Behavior")]
    [DefaultValue(false)]
    [Description("小数点を許可するか")]
    public bool AllowDecimal
    {
      get => allowDecimal;
      set => allowDecimal = value;
    }
    protected override void OnKeyPress(KeyPressEventArgs e)
    {
      base.OnKeyPress(e);
      if (!char.IsDigit(e.KeyChar) &&
          e.KeyChar != (char)Keys.Back &&
          e.KeyChar != (char)Keys.Delete)
      {
          if (allowDecimal && e.KeyChar == '.' &&
            !this.Text.Contains("."))
          {
            e.Handled = false;
          }
          else if (char.IsControl(e.KeyChar))
          {
            e.Handled = false;
          }
          else
          {
            e.Handled = true;
          }
      }
    }
    protected override void OnValidating(CancelEventArgs e)
    {
      base.OnValidating(e);
      if (!string.IsNullOrEmpty(this.Text))
      {
          if (allowDecimal)
          {
            if (!decimal.TryParse(this.Text, out _))
            {
                e.Cancel = true;
                this.BackColor = Color.LightCoral;
            }
            else
            {
                e.Cancel = false;
                this.BackColor = Color.White;
            }
          }
          else
          {
            if (!int.TryParse(this.Text, out _))
            {
                e.Cancel = true;
                this.BackColor = Color.LightCoral;
            }
            else
            {
                e.Cancel = false;
                this.BackColor = Color.White;
            }
          }
      }
    }
}
</syntaxhighlight>
<br>
==== カスタムプログレスバー ====
バーの色と進捗率の表示をカスタマイズできるプログレスバーの例である。<br>
<br>
<syntaxhighlight lang="c#">
public class CustomProgressBar : ProgressBar
{
    private Color barColor = Color.LimeGreen;
    private bool showPercentage = true;
    [Browsable(true)]
    [Category("Appearance")]
    public Color BarColor
    {
      get => barColor;
      set { barColor = value; Invalidate(); }
    }
    [Browsable(true)]
    [Category("Appearance")]
    [DefaultValue(true)]
    public bool ShowPercentage
    {
      get => showPercentage;
      set { showPercentage = value; Invalidate(); }
    }
    public CustomProgressBar()
    {
      this.DoubleBuffered = true;
      SetStyle(ControlStyles.UserPaint, true);
    }
    protected override void OnPaint(PaintEventArgs e)
    {
      e.Graphics.Clear(this.BackColor);
      e.Graphics.FillRectangle(new SolidBrush(Color.LightGray), this.ClientRectangle);
      float percentage = (float)this.Value / this.Maximum;
      int filledWidth = (int)(this.Width * percentage);
      e.Graphics.FillRectangle(new SolidBrush(barColor), 0, 0, filledWidth, this.Height);
      if (showPercentage)
      {
          string text = $"{(int)(percentage * 100)}%";
          var textSize = e.Graphics.MeasureString(text, this.Font);
          var textX = (this.Width - textSize.Width) / 2;
          var textY = (this.Height - textSize.Height) / 2;
          e.Graphics.DrawString(text, this.Font, Brushes.Black, textX, textY);
      }
    }
}
</syntaxhighlight>
<br><br>
{{#seo:
|title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki
|keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,電気回路,電子回路,基板,プリント基板
|description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux
|image=/resources/assets/MochiuLogo_Single_Blue.png
}}


__FORCETOC__
__FORCETOC__
[[カテゴリ:C_Sharp]]
[[カテゴリ:C_Sharp]]

2026年5月13日 (水) 18:53時点における最新版

概要

Windows Formsに於いて、標準のコントロールだけでは要件を満たせない場合、カスタムコントロールを作成して独自のUI要素を実現できる。

カスタムコントロールは大きく3つのアプローチに分類される。

  • User Control (System.Windows.Forms.UserControl継承)
    複数の既存コントロールを組み合わせて単一の再利用可能なコンポーネントとする。
    例: 顧客情報入力パネル (DataGridView + BindingSource + BindingNavigator)
    既存コントロールの機能をそのまま活用でき、実装が容易である。

  • Extended Control (既存コントロール継承: Button, TextBox等)
    既存コントロールの機能を保持しつつ、カスタム機能の追加や外観の変更を行う。
    例: クリック時にテキストを全選択するテキストボックス
    OnPaintオーバーライドで外観もカスタマイズ可能である。

  • Custom Control (System.Windows.Forms.Control継承)
    完全にカスタムな描画と動作を実装する。
    例: アナログ時計コントロール
    最大の柔軟性を持つが、実装コストが大きい。


Windows Formsコントロールの継承階層は、System.Windows.Forms.Control (基底クラス: ウィンドウハンドル管理、メッセージルーティング、マウス / キーボードイベント) を頂点として、
各種コントロール (Button, TextBox, ListBox等) と UserControl (複合コントロール向け) に分岐する構造となっている。


カスタムコントロールのソリューション / プロジェクトの作成

Visual Studioを起動して、[メニューバー] - [ファイル] - [新規作成] - [プロジェクト]を選択する。
[クラスライブラリ]を選択して[OK]ボタンを押下する。

ソリューションとプロジェクトが作成されるが、プロジェクトに初期配置されているClass1.csファイルは不要なので削除する。

<プロジェクト名> の上で右クリックして、[追加] - [新しい項目] - [カスタムコントロール]を選択する。
(右上の検索バーを使用すると簡単に見つけることができる)


カスタムコントロールの作成

ここでは、テキストボックスを拡張したカスタムコントロールを作成する。
仕様は、テキストボックスをクリックするとテキストが全選択状態になるテキストボックスとする。

まず、カスタムコントロールのデザイン画面でイベントを作成する。(プロパティのイベントの[Click]項目を選択する)
作成されたイベントメソッドに、this.SelectAll(); と記述して、クリック時にテキストを全選択できるようにする。

以下に示すように、CustomControl1.csのソースコードを記述する。
ビルドすることでDLLが作成される。

 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
 using System.Drawing;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows.Forms;
 
 namespace ClassLibrary1
 {
    public partial class CustomControl1 : TextBox
    {
       public CustomControl1()
       {
          InitializeComponent();
       }
 
       protected override void OnPaint(PaintEventArgs pe)
       {
          base.OnPaint(pe);
       }
 
       // クリック時にテキストを全選択する
       private void CustomControl1_Click(object sender, EventArgs e)
       {
          this.SelectAll();
       }
    }
 }



カスタムコントロール (DLL) の使用方法

上記で作成したカスタムコンロトール (DLL) を他プロジェクトで使用する。

まず、他ソリューションとして、Windowsフォームプロジェクトを作成する。

次に、[メニューバー] - [ツール] - [ツールボックスアイテムの選択]を選択して、作成したカスタムコントロールをツールボックスに表示させる。
[ツールボックスアイテムの選択]画面が表示されるので、[参照]ボタンを押下して、上記で作成したDLLを選択する。

.NET / .NET Frameworkコンポーネントにカスタムコントロールが追加されたことを確認して、[OK]ボタンを押下する。
[ツールボックス]にカスタムコントロールが配置されるので確認する。

カスタムコントロールをフォームに配置してデバッグを行う。
カスタムコントロールに文字列を入力して、クリックすると全選択されるか確認する。


カスタムコントロールの描画

カスタムコントロールの外観は、OnPaintメソッドをオーバーライドすることで完全に制御できる。
描画処理を適切に実装することにより、標準コントロールには存在しない独自の視覚的表現を実現できる。

OnPaintメソッドのオーバーライド

OnPaint メソッドをオーバーライドすることで、コントロールの描画処理をカスタマイズできる。

引数の PaintEventArgs には、以下に示す情報が含まれる。

  • Graphics
    描画オブジェクト
    線、図形、テキスト等の描画に使用する。
  • ClipRectangle
    再描画が必要な領域を示す矩形
    この領域のみを再描画することで効率化できる。


ClientRectangle はコントロール全体の利用可能な領域を示し、ClipRectangle と異なり常にコントロール全体のサイズを返す。

以下の例では、コントロールの境界に矩形を描画している。

 protected override void OnPaint(PaintEventArgs e)
 {
    base.OnPaint(e);
 
    using var myPen = new Pen(Color.Aqua);
    var area = new Rectangle(new Point(0, 0), new Size(this.Size.Width - 1, this.Size.Height - 1));
    e.Graphics.DrawRectangle(myPen, area);
 }


OnPaintBackgroundメソッド

OnPaintBackground メソッドをオーバーライドすることにより、背景描画を前景描画から分離できる。
背景の描画を独立させることで、コードの見通しが良くなり、ダブルバッファリングとの親和性も高まる。

以下の例では、グラデーション背景を描画している。

 protected override void OnPaintBackground(PaintEventArgs e)
 {
    using var brush = new LinearGradientBrush(
       new Point(0, 0),
       new Point(0, this.Height),
       Color.White,
       Color.LightBlue);
    e.Graphics.FillRectangle(brush, this.ClientRectangle);
 }
 
 protected override void OnPaint(PaintEventArgs e)
 {
    base.OnPaint(e);
    // 前景の詳細描画...
 }


ダブルバッファリング

ダブルバッファリングは、描画のちらつきを防止するための技術である。

描画をオフスクリーンバッファで行い、完成後に一括して画面に転送することで、描画途中の状態が表示されるのを防ぐ。

実装方法は3つある。

  • 方法 1
    DoubleBufferedプロパティ (推奨)
    最も簡単な方法であり、コンストラクタで有効化するだけでよい。
     public MyCustomControl()
     {
        InitializeComponent();
        this.DoubleBuffered = true;
     }
    

  • 方法 2
    SetStyleメソッド
    描画スタイルを細かく制御したい場合に使用する。
     public MyCustomControl()
     {
        InitializeComponent();
        SetStyle(ControlStyles.UserPaint, true);
        SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
     }
    

  • 方法 3
    BufferedGraphicsContext (手動制御)
    描画タイミングを完全に手動制御したい場合に使用する。
     private BufferedGraphicsContext context = BufferedGraphicsManager.Current;
     
     protected override void OnPaint(PaintEventArgs e)
     {
        var bufferedGraphics = context.Allocate(e.Graphics, this.ClientRectangle);
        bufferedGraphics.Graphics.Clear(this.BackColor);
     
        // 描画処理...
     
        bufferedGraphics.Render();
        bufferedGraphics.Dispose();
     }
    


InvalidateとUpdateの使い分け

コントロールの再描画を制御するメソッドは3種類ある。

  • Invalidate()
    指定した領域を「再描画が必要」とマークする。(非同期)
    OnPaintは即座には呼び出されない。
  • Update()
    マークされた領域を即座に同期再描画する。
  • Refresh()
    Invalidate(true) + Update() の短縮形
    コントロール全体を即座に再描画する。


以下に代表的な使用パターンを示す。

 // パターン1: 特定領域の即座な更新
 var dirtyRect = new Rectangle(10, 10, 50, 50);
 this.Invalidate(dirtyRect);
 this.Update();
 
 // パターン2: 複数の変更をまとめて反映
 this.someProperty = newValue1;
 this.anotherProperty = newValue2;
 this.Invalidate();
 this.Update();
 
 // パターン3: コントロール全体の更新
 this.Refresh();



カスタムプロパティの追加

カスタムコントロールには、Visual Studioのプロパティウィンドウで設定可能なカスタムプロパティを追加できる。

プロパティに各種デザイナー属性を付与することにより、プロパティウィンドウへの表示方法を制御できる。

以下の例では、カスタムプロパティを定義している。

 public class MyCustomControl : Control
 {
    private int angle = 0;
    private string placeholderText = "Enter text...";
    private Color cornerColor = Color.Blue;
 
    [Browsable(true)]
    [Category("Appearance")]
    [Description("コントロールの角度 (度単位)")]
    [DefaultValue(0)]
    public int Angle
    {
       get => angle;
       set
       {
          if (angle != value)
          {
             angle = value;
             Invalidate();
          }
       }
    }
 
    [Browsable(false)]
    public string InternalState { get; set; }
 
    [Browsable(true)]
    [Category("Appearance")]
    [Description("テキストが空の時に表示するプレースホルダーテキスト")]
    [DefaultValue("Enter text...")]
    public string PlaceholderText
    {
       get => placeholderText;
       set
       {
          if (placeholderText != value)
          {
             placeholderText = value;
             Invalidate();
          }
       }
    }
 
    [Browsable(true)]
    [Category("Appearance")]
    [Description("コーナー部分の描画色")]
    [DefaultValue(typeof(Color), "Blue")]
    public Color CornerColor
    {
       get => cornerColor;
       set
       {
          if (cornerColor != value)
          {
             cornerColor = value;
             Invalidate();
          }
       }
    }
 }


下表に、主なデザイナー属性を示す。

デザイナー属性一覧
属性 用途
[Browsable(bool)] プロパティウィンドウに表示するか制御する。
[Category(string)] プロパティウィンドウ内のカテゴリ名を設定する。
[Description(string)] プロパティウィンドウ下部に表示される説明文を設定する。
[DefaultValue(object)] デフォルト値を指定する。(デザイナーシリアライゼーション基準値)
[ReadOnly(bool)] プロパティが読み取り専用かを設定する。
[DesignerSerializationVisibility] デザイナーシリアライゼーション動作を設定する。(Content / Hidden / Visible)


TypeConverterの使用

TypeConverter を使用することにより、プロパティウィンドウでのカスタム型の入力 / 変換方法を定義できる。

以下の例では、文字列を Size 型に変換するカスタムコンバーターを定義している。

 public class SizeConverter : TypeConverter
 {
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
       return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }
 
    public override object ConvertFrom(ITypeDescriptorContext context,
       CultureInfo culture, object value)
    {
       if (value is string str)
       {
          var parts = str.Split(',');
          if (parts.Length == 2 &&
             int.TryParse(parts[0].Trim(), out var width) &&
             int.TryParse(parts[1].Trim(), out var height))
          {
             return new Size(width, height);
          }
       }
       return base.ConvertFrom(context, culture, value);
    }
 }
 
 [TypeConverter(typeof(SizeConverter))]
 public Size CustomSize { get; set; }



カスタムイベントの定義

カスタムコントロールでは、標準のイベントに加えて独自のイベントを定義し、フォーム側で処理を受け取ることができる。

EventArgsの拡張

イベントに付加情報を持たせる場合は、EventArgs を継承してカスタムクラスを作成する。

以下の例では、スコア情報を持つイベント引数クラスを定義している。

 public class ScoreEventArgs : EventArgs
 {
    public int Score { get; set; }
    public DateTime ScoreTime { get; set; }
    public string ScoreLevel { get; set; }
 
    public ScoreEventArgs(int score, string level)
    {
       Score = score;
       ScoreLevel = level;
       ScoreTime = DateTime.Now;
    }
 }
 
 public class ValueChangedEventArgs : EventArgs
 {
    public object OldValue { get; set; }
    public object NewValue { get; set; }
 }


イベントの定義と発火

カスタムイベントは EventHandler<T> を使用して定義する。

イベントの発火は protected virtual OnXxx パターンで実装することにより、サブクラスでのオーバーライドを可能にする。

 public class MyCustomControl : Control
 {
    public event EventHandler<ScoreEventArgs> ScoreChanged;
    public event EventHandler CornerClicked;
 
    protected virtual void OnScoreChanged(ScoreEventArgs e)
    {
       ScoreChanged?.Invoke(this, e);
    }
 
    protected virtual void OnCornerClicked(EventArgs e)
    {
       CornerClicked?.Invoke(this, e);
    }
 
    private int score = 0;
    public int Score
    {
       get => score;
       set
       {
          if (score != value)
          {
             score = value;
             var args = new ScoreEventArgs(score, GetScoreLevel(score));
             OnScoreChanged(args);
          }
       }
    }
 
    protected override void OnClick(EventArgs e)
    {
       base.OnClick(e);
       var mousePos = this.PointToClient(MousePosition);
       if (IsCornerArea(mousePos))
       {
          OnCornerClicked(e);
       }
    }
 
    private bool IsCornerArea(Point p)
    {
       const int cornerSize = 20;
       return (p.X < cornerSize && p.Y < cornerSize) ||
              (p.X > this.Width - cornerSize && p.Y < cornerSize) ||
              (p.X < cornerSize && p.Y > this.Height - cornerSize) ||
              (p.X > this.Width - cornerSize && p.Y > this.Height - cornerSize);
    }
 
    private string GetScoreLevel(int score)
    {
       return score switch
       {
          >= 90 => "Excellent",
          >= 70 => "Good",
          >= 50 => "Fair",
          _ => "Low"
       };
    }
 }


イベントの使用例

フォーム側でカスタムイベントのハンドラを登録する方法を示す。

 public partial class MainForm : Form
 {
    public MainForm()
    {
       InitializeComponent();
       var customControl = new MyCustomControl();
       customControl.ScoreChanged += CustomControl_ScoreChanged;
       customControl.CornerClicked += CustomControl_CornerClicked;
       this.Controls.Add(customControl);
    }
 
    private void CustomControl_ScoreChanged(object sender, ScoreEventArgs e)
    {
       MessageBox.Show($"Score changed to {e.Score} ({e.ScoreLevel}) at {e.ScoreTime:HH:mm:ss}");
    }
 
    private void CustomControl_CornerClicked(object sender, EventArgs e)
    {
       MessageBox.Show("You clicked a corner!");
    }
 }



デザイナーサポート

Visual Studioのデザイナーと連携することで、カスタムコントロールを設計時に視覚的に配置・設定できる。

デザイナーサポートを適切に実装することにより、コントロールの利便性が大幅に向上する。

DesignModeプロパティ

DesignMode プロパティを使用することで、設計時と実行時で異なる描画 / 動作を実装できる。

設計時はプレースホルダーを表示し、実行時はデータを表示するといった使い分けが可能である。

※注意
DesignMode プロパティはコンストラクタ内では使用できない。
サイト割り当て前のため、常に false を返す。

 public class MyCustomControl : Control
 {
    protected override void OnPaint(PaintEventArgs e)
    {
       base.OnPaint(e);
 
       if (this.DesignMode)
       {
          e.Graphics.DrawString("MyCustomControl",
             this.Font, Brushes.Gray, new PointF(5, 5));
       }
       else
       {
          // 実行時の描画処理
       }
    }
 }


DesignerSerializationVisibility属性

DesignerSerializationVisibility 属性を使用して、デザイナーがプロパティをどのようにシリアライズするかを制御できる。

  • Content
    コレクション等のプロパティの内容をシリアライズする。
  • Visible
    プロパティの値をシリアライズする。(デフォルト動作)
  • Hidden
    プロパティをシリアライズしない。(実行時のみのプロパティに使用する)


 public class MyCustomControl : Control
 {
    private List<string> items = new List<string>();
 
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public List<string> Items { get => items; }
 
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public int CustomValue { get; set; }
 
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public string RuntimeOnlyValue { get; set; }
 }


カスタムデザイナー

Designer 属性と ControlDesigner を継承することにより、Visual Studioデザイナーの動作を詳細にカスタマイズできる。

 [Designer(typeof(MyCustomControlDesigner))]
 public class MyCustomControl : Control
 {
    // コントロール実装
 }
 
 public class MyCustomControlDesigner : ControlDesigner
 {
    public override void Initialize(IComponent component)
    {
       base.Initialize(component);
    }
 
   protected override void WndProc(ref Message m)
    {
       base.WndProc(ref m);
    }
 }



高度なテクニック

WndProcのオーバーライド

WndProc をオーバーライドすることにより、Windowsメッセージを直接処理できる。

標準のイベントでは対応できない低レベルな操作が必要な場合に使用する。

※注意
処理しないメッセージは必ず base.WndProc() に転送しなければならない。
転送しないとコントロールの基本機能が破損する。

 public class MyCustomControl : Control
 {
    private const int WM_KEYUP = 0x101;
    private const int WM_MOUSEMOVE = 0x200;
    private const int WM_LBUTTONDOWN = 0x201;
    private const int WM_PAINT = 0x0F;
 
    protected override void WndProc(ref Message m)
    {
       switch (m.Msg)
       {
          case WM_KEYUP:
             if ((int)m.WParam == (int)Keys.Escape)
             {
                MessageBox.Show("ESCキーが押されました");
             }
             break;
          case WM_MOUSEMOVE:
             int x = (int)m.LParam & 0xFFFF;
             int y = ((int)m.LParam >> 16) & 0xFFFF;
             break;
          case WM_LBUTTONDOWN:
             break;
          default:
             base.WndProc(ref m);
             break;
       }
    }
 }


オーナードロー

オーナードローを使用することで、ListBox等の標準コントロールの各項目を独自に描画できる。

DrawMode.OwnerDrawFixed を設定し、OnDrawItem をオーバーライドして描画処理を実装する。

以下の例では、カスタム描画を行うListBoxを定義している。

 public class MyCustomListBox : ListBox
 {
    public MyCustomListBox()
    {
       this.DrawMode = DrawMode.OwnerDrawFixed;
       this.ItemHeight = 30;
    }
 
    protected override void OnDrawItem(DrawItemEventArgs e)
    {
       if (e.Index < 0) return;
 
       e.DrawBackground();
 
       string text = this.Items[e.Index].ToString();
       Color textColor = (e.State & DrawItemState.Selected) != 0 ? Color.White : Color.Black;
 
       using (var brush = new SolidBrush(textColor))
       {
          e.Graphics.DrawString(text, e.Font, brush, e.Bounds.Left + 5, e.Bounds.Top + 5);
       }
 
       e.DrawFocusRectangle();
    }
 }


複合コントロール

UserControl をベースにして、複数の既存コントロールを1つのコンポーネントとして組み合わせることができる。

以下の例では、住所入力パネルの複合コントロールを定義している。

 [Designer(typeof(ParentControlDesigner))]
 public class AddressControl : UserControl
 {
    private TextBox nameTextBox;
    private TextBox addressTextBox;
    private TextBox phoneTextBox;
    private Label nameLabel;
    private Label addressLabel;
    private Label phoneLabel;
 
    public AddressControl()
    {
       InitializeComponent();
    }
 
    private void InitializeComponent()
    {
       nameLabel = new Label { Text = "Name:" };
       nameTextBox = new TextBox();
 
       addressLabel = new Label { Text = "Address:" };
       addressTextBox = new TextBox { Multiline = true };
 
       phoneLabel = new Label { Text = "Phone:" };
       phoneTextBox = new TextBox();
 
       this.Controls.AddRange(new Control[]
       {
          nameLabel, nameTextBox,
          addressLabel, addressTextBox,
          phoneLabel, phoneTextBox
       });
    }
 
    [Browsable(true)]
    [Category("Data")]
    public string PersonName
    {
       get => nameTextBox.Text;
       set => nameTextBox.Text = value;
    }
 
    public string Address
    {
       get => addressTextBox.Text;
       set => addressTextBox.Text = value;
    }
 
    public string Phone
    {
       get => phoneTextBox.Text;
       set => phoneTextBox.Text = value;
    }
 }



.NET 8以降のWindows Forms

.NET 8以降では、Windows FormsのAPIが大幅に改善されており、カスタムコントロール開発においても恩恵を受けられる。

.NET 8

新しいデータバインディングエンジンが導入された。

ICommand インターフェースのサポートが追加され、Button.Command プロパティに ICommand を割り当てられるようになった。
これによりMVVM設計パターンがWindows Formsでも実装しやすくなった。

.NET 9

以下に示す機能が追加または変更された。

  • 非同期フォーム機能
    UI関連の操作を非同期で実行できるAPI群が追加された。
  • ダークモード対応 (実験的)
    アプリケーションがダークテーマに自動適応する機能
  • BinaryFormatterの削除
    セキュリティリスク排除のため完全に削除された。
  • ToolStrip.AllowClickThrough
    フォーム非フォーカス状態でToolStripを操作可能になった。


移行時の注意点

.NET Frameworkから.NET 8以降へ移行する場合は、以下に示す点に注意する。

  • フォームとカスタムコントロールのレイアウトを確認する。
    フォント差異によるレイアウトへの影響が発生する場合がある。

  • サードパーティコントロールの.NET 8以降の対応版を確認する
    互換性のないコントロールは代替を検討する必要がある。

  • Microsoft.Windows.Compatibility NuGetパッケージで約20,000 APIを補完できる
    .NET Frameworkにのみ存在するAPIの互換レイヤーを提供する。

  • ターゲットフレームワークはWindows専用 (netX.0-windows) となる
    クロスプラットフォーム実行はできない。



NuGetパッケージとしての配布

作成したカスタムコントロールライブラリをNuGetパッケージとして配布することにより、他のプロジェクトや開発者に共有できる。

.csprojの設定

.csprojファイル に必要なパッケージメタデータを設定する。

 <PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <IsToolboxControlLibrary>true</IsToolboxControlLibrary>
    <Title>My Custom Controls Library</Title>
    <Description>カスタムコントロールライブラリ説明</Description>
    <Authors>開発者名または組織名</Authors>
    <Version>1.0.0</Version>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/your/repo</PackageProjectUrl>
    <RepositoryUrl>https://github.com/your/repo</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <PackageIcon>icon.png</PackageIcon>
 </PropertyGroup>


デザイナーアセンブリのフォルダ構造

Visual Studioのデザイナーでコントロールを使用可能にするためには、以下に示すフォルダ構造でDLLを配置する。

 lib/
    net8.0-windows/
       MyControls.dll
       MyControls.xml
       Design/
          WinForms/
             MyControls.Design.dll
             MyControls.Design.xml


配布方法

パッケージの配布先に応じて、以下に示す方法を使用する。

  • NuGet.org (公開配布)
    dotnet nuget push MyControls.1.0.0.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json

  • 社内NuGetフィード (プライベート配布)
    組織内の専用フィードサーバを利用する。

  • GitHub Packages
    GitHubリポジトリと連携したパッケージ管理を利用する。



実践的な例

プレースホルダー付きテキストボックス

テキストが空の場合にグレーのヒントテキストを表示するテキストボックスの例である。

 public class PlaceholderTextBox : TextBox
 {
    private string placeholder = "Enter text...";
    private bool showPlaceholder = true;
 
    [Browsable(true)]
    [Category("Appearance")]
    [Description("プレースホルダーテキスト")]
    public string Placeholder
    {
       get => placeholder;
       set
       {
          placeholder = value;
          if (showPlaceholder)
             Invalidate();
       }
    }
 
    public PlaceholderTextBox()
    {
       this.Enter += (s, e) => { if (showPlaceholder) this.Text = ""; };
       this.Leave += (s, e) =>
       {
          if (string.IsNullOrEmpty(this.Text))
          {
             this.Text = placeholder;
             showPlaceholder = true;
          }
       };
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
       base.OnPaint(e);

       if (showPlaceholder && string.IsNullOrEmpty(this.Text))
       {
          using (var brush = new SolidBrush(Color.Gray))
          {
             e.Graphics.DrawString(placeholder, this.Font, brush, 3, 3);
          }
       }
    }
 }


カスタムボタン (角丸・グラデーション)

角丸とグラデーション塗りつぶしを持つカスタムボタンの例である。

 public class RoundGradientButton : Button
 {
    private Color gradientColor1 = Color.SteelBlue;
    private Color gradientColor2 = Color.LightBlue;
    private int cornerRadius = 15;
 
    [Browsable(true)]
    [Category("Appearance")]
    public Color GradientColor1
    {
       get => gradientColor1;
       set { gradientColor1 = value; Invalidate(); }
    }
 
    [Browsable(true)]
    [Category("Appearance")]
    public Color GradientColor2
    {
       get => gradientColor2;
       set { gradientColor2 = value; Invalidate(); }
    }
 
    [Browsable(true)]
    [Category("Appearance")]
    [DefaultValue(15)]
    public int CornerRadius
    {
       get => cornerRadius;
       set { cornerRadius = value; Invalidate(); }
    }
 
    public RoundGradientButton()
    {
       this.FlatStyle = FlatStyle.Flat;
       this.FlatAppearance.BorderSize = 0;
       SetStyle(ControlStyles.ResizeRedraw, true);
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
       e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
 
       var path = GetRoundedRectanglePath(this.ClientRectangle, cornerRadius);
 
       using (var brush = new LinearGradientBrush(
          new Point(0, 0),
          new Point(0, this.Height),
          gradientColor1,
          gradientColor2))
       {
          e.Graphics.FillPath(brush, path);
       }
 
       using (var pen = new Pen(Color.DarkGray, 1))
       {
          e.Graphics.DrawPath(pen, path);
       }
 
       var textSize = e.Graphics.MeasureString(this.Text, this.Font);
       var textX = (this.Width - textSize.Width) / 2;
       var textY = (this.Height - textSize.Height) / 2;
 
       using (var brush = new SolidBrush(this.ForeColor))
       {
          e.Graphics.DrawString(this.Text, this.Font, brush, textX, textY);
       }
    }
 
    private GraphicsPath GetRoundedRectanglePath(Rectangle rect, int radius)
    {
       var path = new GraphicsPath();
       var d = radius * 2;
 
       path.AddArc(rect.X, rect.Y, d, d, 180, 90);
       path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
       path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
       path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
       path.CloseFigure();
 
       return path;
    }
 }


数値入力専用テキストボックス

数値以外の入力を拒否し、入力値の検証も行うテキストボックスの例である。

 public class NumericTextBox : TextBox
 {
    private bool allowDecimal = false;
 
    [Browsable(true)]
    [Category("Behavior")]
    [DefaultValue(false)]
    [Description("小数点を許可するか")]
    public bool AllowDecimal
    {
       get => allowDecimal;
       set => allowDecimal = value;
    }
 
    protected override void OnKeyPress(KeyPressEventArgs e)
    {
       base.OnKeyPress(e);
 
       if (!char.IsDigit(e.KeyChar) &&
          e.KeyChar != (char)Keys.Back &&
          e.KeyChar != (char)Keys.Delete)
       {
          if (allowDecimal && e.KeyChar == '.' &&
             !this.Text.Contains("."))
          {
             e.Handled = false;
          }
          else if (char.IsControl(e.KeyChar))
          {
             e.Handled = false;
          }
          else
          {
             e.Handled = true;
          }
       }
    }
 
    protected override void OnValidating(CancelEventArgs e)
    {
       base.OnValidating(e);
 
       if (!string.IsNullOrEmpty(this.Text))
       {
          if (allowDecimal)
          {
             if (!decimal.TryParse(this.Text, out _))
             {
                e.Cancel = true;
                this.BackColor = Color.LightCoral;
             }
             else
             {
                e.Cancel = false;
                this.BackColor = Color.White;
             }
          }
          else
          {
             if (!int.TryParse(this.Text, out _))
             {
                e.Cancel = true;
                this.BackColor = Color.LightCoral;
             }
             else
             {
                e.Cancel = false;
                this.BackColor = Color.White;
             }
          }
       }
    }
 }


カスタムプログレスバー

バーの色と進捗率の表示をカスタマイズできるプログレスバーの例である。

 public class CustomProgressBar : ProgressBar
 {
    private Color barColor = Color.LimeGreen;
    private bool showPercentage = true;
 
    [Browsable(true)]
    [Category("Appearance")]
    public Color BarColor
    {
       get => barColor;
       set { barColor = value; Invalidate(); }
    }
 
    [Browsable(true)]
    [Category("Appearance")]
    [DefaultValue(true)]
    public bool ShowPercentage
    {
       get => showPercentage;
       set { showPercentage = value; Invalidate(); }
    }
 
    public CustomProgressBar()
    {
       this.DoubleBuffered = true;
       SetStyle(ControlStyles.UserPaint, true);
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
       e.Graphics.Clear(this.BackColor);
 
       e.Graphics.FillRectangle(new SolidBrush(Color.LightGray), this.ClientRectangle);
 
       float percentage = (float)this.Value / this.Maximum;
       int filledWidth = (int)(this.Width * percentage);
 
       e.Graphics.FillRectangle(new SolidBrush(barColor), 0, 0, filledWidth, this.Height);
 
       if (showPercentage)
       {
          string text = $"{(int)(percentage * 100)}%";
          var textSize = e.Graphics.MeasureString(text, this.Font);
          var textX = (this.Width - textSize.Width) / 2;
          var textY = (this.Height - textSize.Height) / 2;
 
          e.Graphics.DrawString(text, this.Font, Brushes.Black, textX, textY);
       }
    }
 }