.NET 対応コンポーネントを作る

Last Updated 2011/09/21


コンポーネント、クラス、コントロールの区別は微妙なところですが、それらを総称してコンポーネントと呼ぶことにします。コンポーネントは独立性が高く、アプリケーションプログラムとは public なプロパティやメソッドを通じてコミュニケーションを取ります。

自作のコンポーネントはここで公開していますが、ユーティリティ的なもの、つまり、チョット高度な機能を持つ関数のようなものばかりです。このページではもう少しコンポーネントらしいものを作る手順について書きたいと思います。

解説文中のプロパティやメソッドについては説明しませんが、私が公開している .Net Framework クラスライブラリリファレンスを見ながらサンプルコードを見ると、より分かりやすいと思います。

なお、WPF 対応のビジュアルコントロールは、.NET 対応のものとは設計思想が異なります。詳しくは、WPF のページをご覧いただくとして、.NET コントロールを WPF アプリケーションで使用することは可能です。


Component クラスから派生するクラス

非ビジュアルコントロールを作成するときは通常、Component クラスから派生します。フォームに貼り付けて使わないコントロールはあえて Component クラスから派生する必然性はありませんが、各プロパティをプロパティウインドウ内で設定できる点と、イベントハンドラのテンプレートコードを自動的に作成するなどのメリットもあります。

Component クラスから派生するクラスの例として、FindFileEx コントロールを取り上げます。これは指定のフォルダ内のファイルまたはフォルダを検索し、検索に成功するとイベントを発生するものです。

ファイルやフォルダを検索する .Net Framework の機能としては DirectoryInfoクラスなどがありますが、これらに共通した欠点は目的のファイルまたはフォルダが見つかったところで検索を中止したくてもできません。そこで、このコントロールでは API 関数を使いました。

プロパティ エディタ

Visual Studio の IDE は public なプロパティに対しては自動的にプロパティエディタを起動してくれます。たとえば、Color 型のプロパティには色を選択できるドロップダウンリストが表示されます。enum 型の場合は enum 型の値名をリストアップします。ほとんどの場合、標準のプロパティエディタで間に合うと思いますが、独自のプロパティエディタが必要な場合もあります。

HatchedPanel コントロールは Panel クラスから派生し、指定の HatchBrush オブジェクトでクライアント領域内を塗りつぶすものです。コントロールとしての実用性はありませんが、HatchStyle プロパティをビジュアルに設定できるプロパティエディタを付けておきました。

既存の Windows フォームコントロールから派生するコントロール

既存のコントロールにチョット手を加えれば間に合うような場合は既存のコントロールから派生する方法がもっとも簡単で、安全性も高いといえます。

GroupBoxEx コントロールは GroupBox コントロールから派生するコントロールで、GroupBox コントロールと RadioButton コントロールとを組み合わせたコントロールです。つまり、グループボックス内にラジオボタンを配置するとき、ラジオボタンのキャプションを文字列の配列として入力すると、ラジオボタンコントロールを自動的に作成し、グループボックス内に均等割りで配置します。

コントロールのインストール

作成したコントロールが実用的に使える段階になればツールボックスに登録し、アプリケーションの開発にいつても利用できるようにすることになります。これをコントロールのインストールと呼ぶことにし、その手順について説明します。

カラー選択コンボボックス

ColorComboBox コントロールは、ComboBox クラスから派生するコントロールです。140 色の WEB カラーをコンボボックスにオーナー描画しています。プロパティの一部をプロパティウインドウに表示しないようにするテクニックと、コントロールデザイナを使って一部のプロパティを無効にするテクニックを含みます。

カラー選択コントロール

ColorSelector コントロールは、UserControl クラスから派生するコントロールです。プロパティウインドウの Color 型のプロパティを設定するときにドロップダウンするカラー選択ダイアログボックスとほぼ同じものです。

コントロールデザイナ

コントロールデザイナは、Visual Studio などのビジュアルな開発環境でフォームやコントロールを設計する仕組みです。


コードのクラス化

Components のページで自作のクラスの紹介とともに、詳しく説明していますが、まとまった処理を 1 つのクラスにしておくとコードの再利用性が高まります。Windows フォームアプリケーションの場合、極論すれば、フォームユニットには各コントロールのイベントハンドラだけ書いておいて、実際の処理はすべてクラス化してもいいぐらいです。

コードのクラス化のメリットは再利用性のほかに、デバッグの容易さ(すでにチェック済みのクラスの部分はデバッグする必要がない)やコードのコンパクト化などを挙げることができます。小規模なアプリケーションならともかく、少しまとまったアプリケーションの場合はコードのクラス化を前提にして開発することをすすめます。


Component クラスから派生するコントロール(FindFileEx コントロール)

指定のフォルダ内のファイルまたはフォルダを列挙するコントロールです。テスト用のアプリケーションを含めてソースコードは、Components のページでダウンロードできます。

このコントロールの特徴はファイルまたはフォルダの検索に成功したとき、イベントを発生する点です。目的のファイルまたはフォルダが見つかったところで検索を中止したいときはこのイベントハンドラ内で Stop メソッドを呼び出します。

なお、このコンポーネントでは、Windows API 関数を使いますので、API 関数を使う手順についても学ぶことができます。

namespace emanual.Control
{
  public class FindFileEx : System.ComponentModel.Component
  {
    // デリゲートの宣言
    public delegate void FindFileEventHandler(object sender, FindFileEventArgs e);

    // イベントハンドラの宣言
    public event FindFileEventHandler FindFile;

    private bool FCancel;         // 検索をキャンセルするとき true
    private string FCriteria;     // 検索の条件(ワイルドカード可)
    private string FSearchFolder; // 検索の対象とするフォルダ名
    private bool FWantFolder;     // フォルダだけを検索するとき true(false のとき、ファイルだけを検索する)

    // プロパティ
    public string Criteria { get { return FCriteria; } set { FCriteria = value; } }

    // フォルダ選択ダイアログボックスを開く
    [EditorAttribute(typeof(System.Windows.Forms.Design.FolderNameEditor), typeof(System.Drawing.Design.UITypeEditor))]
    public string SearchFolder { get { return FSearchFolder; } set { FSearchFolder = value; } }

    public bool WantFolder { get { return FWantFolder; } set { FWantFolder = value; } }

    //-------------------------------------------------------------------------------------
    // コンストラクタ
    public FindFileEx()
    {
      FSearchFolder = @"c:\"; //デフォルトの設定
      FCriteria = "*";
      FWantFolder = false;
    }

    //-------------------------------------------------------------------------------------
    // 検索を開始する
    public void Start()
    {
      bool check = false;
      string condition;
      FCancel = false;

      if (!FSearchFolder.EndsWith("\\"))
        FSearchFolder += "\\";

      // 検索の条件
      condition = Path.Combine(FSearchFolder, FCriteria);

      api.WIN32_FIND_DATA fd = new api.WIN32_FIND_DATA();

      // 最初の検索を実行し、検索ハンドルを取得する
      IntPtr handle = api.FindFirstFile(condition, fd);

      if (handle != null)
      {
        do
        {
          if (FindFile != null)
            this.OnFindFile(fd);

          if (FCancel)
            break;

          // 次の検索を実行する
          check = api.FindNextFile(handle, fd);
        }
        while (check);
      }

      // 検索ハンドルを開放する
      api.FindClose(handle);
    }

    //-------------------------------------------------------------------------------------
    // 検索を中止する
    public void Stop()
    {
      FCancel = true;
    }

    //-------------------------------------------------------------------------------------
    // FindFile イベントを発生する
    protected virtual void OnFindFile(api.WIN32_FIND_DATA fd)
    {
      FindFileEventArgs e = null;

      if (FWantFolder) // フォルダだけを検索するとき
      {
        if ((fd.dwFileAttributes & (int)FileAttributes.Directory) == (int)FileAttributes.Directory)
          e = new FindFileEventArgs(fd);
      }
      else // ファイルだけを検索するとき
      {
        if ((fd.dwFileAttributes & (int)FileAttributes.Directory) != (int)FileAttributes.Directory)
          e = new FindFileEventArgs(fd);
      }

      // FindFile イベントにデータを渡す
      if (e != null)
        FindFile(this, e);
    }

    //**************************************************************************************
    // FindFileEventArgs class
    //**************************************************************************************
    public class FindFileEventArgs : EventArgs
    {
      private readonly api.WIN32_FIND_DATA FFindData;
      public api.WIN32_FIND_DATA FindData { get { return FFindData; } }

      public FindFileEventArgs(api.WIN32_FIND_DATA findData)
      {
        FFindData = findData;
      }
    } // end of FindFileEventArgs class

    //**************************************************************************************
    // api class
    //**************************************************************************************
    public class api
    {
      [StructLayout(LayoutKind.Sequential)]
      public struct FILETIME
      {
        public int dwLowDateTime;
        public int dwHighDateTime;
      }

      [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
      public class WIN32_FIND_DATA
      {
        public int dwFileAttributes;
        public FILETIME ftCreationTime;
        public FILETIME ftLastAccessTime;
        public FILETIME ftLastWriteTime;
        public int nFileSizeHigh;
        public int nFileSizeLow;
        public int dwReserved0;
        public int dwReserved1;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string cFileName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
        public string cAlternateFileName;

        public WIN32_FIND_DATA()
        {
          dwFileAttributes = 0;
          FILETIME fileTime = new FILETIME();
          fileTime.dwLowDateTime = 0;
          fileTime.dwHighDateTime = 0;
          ftCreationTime = fileTime;
          ftLastAccessTime = fileTime;
          ftLastWriteTime = fileTime;
          nFileSizeHigh = 0;
          nFileSizeLow = 0;
          dwReserved0 = 0;
          dwReserved1 = 0;
          cFileName = String.Empty;
          cAlternateFileName = String.Empty;
        }
      } // end of WIN32_FIND_DATA class

      [DllImport("kernel32.dll", SetLastError = true)]
      public static extern bool FindClose(IntPtr hndFindFile);

      [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
      public static extern IntPtr FindFirstFile(string pFileName, [In, Out] WIN32_FIND_DATA pFindFileData);

      [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
      public static extern bool FindNextFile(IntPtr hndFindFile, [In, Out] WIN32_FIND_DATA lpFindFileData);
    } // end of api class
  } // end of FindFile class
} // end of emanual.Control namespace

プロパティエディタ(HatchedPanel コントロール)

PropEditor1 PropEditor

左図はフォームに貼り付けた HatchedPanel コントロールのデザイン時の状態です。右図はプロパティウインドウの一部ですが、HatchedPanel コントロールに関係するプロパティは BackgroudnColor、ForegroundColor、HatchStyle の各プロパティです。特に、HatchStyle プロパティの項に注目してください。このプロパティを設定するときは以下に示すようなビジュアルなプロパティエディタを表示します。

PropEditor2

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing; // Color
using System.Drawing.Drawing2D; // HatchBrush
using System.Windows.Forms; // Panel
using System.ComponentModel; // BrowsableAttibute
using System.Security.Permissions; // SecurityAction
using System.Drawing.Design; // UITypeEditor
using System.Windows.Forms.Design; // IWindowsFormsEditorService
//using System.Globalization; // CultureInfo

namespace emanual.Control
{
  public class HatchedPanel : System.Windows.Forms.Panel
  {
    private HatchBrush FHatchBrush = null;
    private HatchStyle FHatchStyle = HatchStyle.BackwardDiagonal;
    private Color FBackgroundColor = Color.White;
    private Color FForegroundColor = Color.Black;

    [Browsable(true)]
    [EditorAttribute(typeof(HatchStyleEditor), typeof(UITypeEditor))]
    public HatchStyle HatchStyle { get { return FHatchStyle; } set { FHatchStyle = value; this.Refresh();}}

    [Browsable(false)]
    public HatchBrush HatchBrush { get { return FHatchBrush; } set { FHatchBrush = value; } }

    public Color BackgroundColor { get { return FBackgroundColor; } set { FBackgroundColor = value; this.Refresh(); } }
    public Color ForegroundColor { get { return FForegroundColor; } set { FForegroundColor = value; this.Refresh(); } }

    //-------------------------------------------------------------------------------------
    // コンストラクタ
    public HatchedPanel()
    {
      FHatchBrush = new HatchBrush(FHatchStyle, FForegroundColor, FBackgroundColor);
    }

    //-------------------------------------------------------------------------------------
    protected override void OnPaint(PaintEventArgs e)
    {
      if (FHatchBrush != null)
      {
        FHatchBrush = new HatchBrush(FHatchStyle, FForegroundColor, FBackgroundColor);

        e.Graphics.FillRectangle(FHatchBrush, this.ClientRectangle);
      }

      base.OnPaint(e);
    }

    //-------------------------------------------------------------------------------------
  } // end of HatchedPanel class

  //**************************************************************************************
  // HatchStyleEditor class(プロパティエディタクラス)
  //**************************************************************************************
  [System.Security.Permissions.PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
  internal class HatchStyleEditor : UITypeEditor
  {
    public HatchStyleEditor()
    {
    }

    //-----------------------------------------------------------------------------------
    // エディタのスタイルを返す
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
      return UITypeEditorEditStyle.DropDown; // ドロップダウン型を指定する
    }

    //-----------------------------------------------------------------------------------
    // プロパティエディタを開いてプロパティを編集しようとするときに呼び出されるメソッド
    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
      // ここはおきまりの手続きと理解すること
      // Visual Studio の内部処理の問題なのでブラックボックス化しているため
      IWindowsFormsEditorService service =
          (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));

      if (service == null)
        return value;

      HatchStyleEditorControl control = new HatchStyleEditorControl((HatchStyle)value, service);
      control.Initialize((HatchStyle)value, service);

      // コントロールをドロップダウンする
      service.DropDownControl(control);

      // プロパティを編集後の結果を返す
      return control.HatchStyle;
    }

    //-----------------------------------------------------------------------------------
    // PaintValue メソッドを有効にするかどうかを返す
    public override bool GetPaintValueSupported(ITypeDescriptorContext context)
    {
      return true; // PaintValue メソッドを有効にする
    }

    //-----------------------------------------------------------------------------------
    // [プロパティ] ウインドウのプロパティ項目内に選択した HatchStyle を描画する
    public override void PaintValue(PaintValueEventArgs e)
    {
      base.PaintValue(e);

      HatchStyle style = (HatchStyle)e.Value;
      HatchBrush brush =  new HatchBrush(style, Color.Black, Color.White);

      e.Graphics.FillRectangle(brush, e.Bounds);
    }
    //-----------------------------------------------------------------------------------
  } // end of HatchStyleEditor class

  //**************************************************************************************
  // HatchStyleEditorControl class(プロパティウインドウ内でドロップダウンするコントロール)
  //**************************************************************************************
  internal class HatchStyleEditorControl : ListBox
  {
    private const int SIZE = 28; // リスト項目の高さ
    private IWindowsFormsEditorService FService;
    private HatchStyle FHatchStyle;

    public HatchStyle HatchStyle { get { return FHatchStyle; } set { FHatchStyle = value; } }

    //-----------------------------------------------------------------------------------
    // コンストラクタ
    public HatchStyleEditorControl(HatchStyle style, IWindowsFormsEditorService service)
    {
      FService = service;
      FHatchStyle = style;

      this.DrawMode = DrawMode.OwnerDrawFixed;
      this.BorderStyle = BorderStyle.None;
      this.ItemHeight = SIZE;

      // リスト項目を設定する
      this.SetItemsToListBox();
    }

    //-----------------------------------------------------------------------------------
    protected override void OnDrawItem(DrawItemEventArgs e)
    {
      base.OnDrawItem(e);

      if (e.Index != -1)
      {
        HatchStyle style = (HatchStyle)this.Items[e.Index];
        string s = Enum.GetName(typeof(HatchStyle), style);
        HatchBrush brush = new HatchBrush(style, Color.Blue, Color.Lavender);

        e.DrawBackground();
        e.Graphics.FillRectangle(brush, new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, SIZE - 4, SIZE - 4));
        e.Graphics.DrawString(s, this.Font, SystemBrushes.WindowText, e.Bounds.X + SIZE, e.Bounds.Y + 2);
        e.DrawFocusRectangle();
      }
    }

    //-----------------------------------------------------------------------------------
    protected override void OnClick(EventArgs e)
    {
      base.OnClick(e);
      FHatchStyle = (HatchStyle)this.SelectedItem;

      // ドロップダウンしたエディタコントロールを閉じる
      FService.CloseDropDown();
    }

    //-----------------------------------------------------------------------------------
    // リスト項目の選択をキーボード操作([Return] キー)でも可能にする
    protected override bool ProcessDialogKey(Keys keyData)
    {
      if (((keyData & Keys.KeyCode) == Keys.Return) && ((keyData & (Keys.Alt | Keys.Control)) == Keys.None))
      {
        this.OnClick(EventArgs.Empty);
        return true;
      }

      return base.ProcessDialogKey(keyData);
    }

    //-----------------------------------------------------------------------------------
    // リスト項目の初期値を選択状態にする
    public void Initialize(HatchStyle style, IWindowsFormsEditorService service)
    {
      FService = service;
      FHatchStyle = style;

      for (int i = 0; i < this.Items.Count; ++i)
      {
        if ((HatchStyle)this.Items[i] == style)
        {
          this.SelectedIndex = i;
          return;
        }
      }
    }

    //-----------------------------------------------------------------------------------
    // リスト項目を設定する
    private void SetItemsToListBox()
    {
      foreach (HatchStyle style in Enum.GetValues(typeof(HatchStyle)))
      {
        this.Items.Add(style);
      }
    }

    //-----------------------------------------------------------------------------------
  } // end of HatchStyleEditorControl class
} // end of namespace

このコントロール自体は実用性が乏しいものですが、テスト用アプリケーションを含めてソースコードを以下のリンクからダウンロードしてください。

HatchedPanel.lzh (65,971 bytes)


GroupBoxEx コントロール

ラジオボタンを使う場合はたいていの場合、グループボックス内に配置します。GroupBoxEx コントロールはラジオボタンのキャプションを文字列の配列として入力するとラジオボタンコントロールを自動的に作成し、グループボックス内に均等に配置します。これは Delphi/C++ Builder で使っている VCL "Visual Component Library" に含まれる TRadioGroup コントロールをまねたものです。

GroupBoxEx

実行時の状態は上図のとおりですが、ラジオボタンは string[] 型の Items プロパティを入力するだけで、グループボックス内の縦方向に均等に配置します。どのラジオボタンをチェックしたかは SelectedIndex プロパティで得ることができます。

なお、このコントロールのソースコードは Components のページにあります。


ColorComboBox コントロール

ComboBox クラスから派生するコントロールで、140 色の WEB カラーを選択可能なコンボボックスです。ソースコードは Compo のページにあります。 

ColorComboBox

このコントロールでは ControlDesigner クラスから派生するコントロールデザイナを使うことで、不要なプロパティをプロパティウインドウへの表示を抑制します。また、一部のプロパティをプロパティウインドウに表示しないテクニックを含みます。


ColorSelector コントロール

UserControl クラスから派生するコントロールです。.Net Framework のヘルプの中に、UserControl クラスから派生するコントロールに対して、「複合コントロール」と呼んでいる箇所があります。これはユーザーコントロールの本質を突いていると思います。つまり、複数の既存のコントロールを組み合わせて、あたかも一つのコントロールのように振舞うコントロールだからです。

UserControl クラスの継承関係をみると、ほとんど Form クラスと同じです。フォームと同様に、ユーザーコントロールにはコントロールデザイナが用意されていますので、フォームのデザインと同じ手順でコントロールを配置することができます。

下図は、フォームに ColorSelector コントロールを配置したところです。

ColorSelector

見てのとおり、プロパティウインドウで Color 型のプロパティを選択するときに表示されるものに似ていますね。事実、ほとんど同じものです。ソースコードは Components のページにあります。

−以上−