DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

プラグイン機能を持つアプリケーションを作成する

補足:CodeZineの「.NETによるプラグイン機能を持つテキストエディタの作成」でも詳しく説明しています。

Adobe PhotoshopBecky! Internet Mailなどのアプリケーションでは、「プラグイン」(または、「アドイン」、「エクステンション」等)と呼ばれるプログラムをインストールすることにより、機能を追加することができるようになっています。ここでは、このようなプラグイン機能を持ったアプリケーションの作り方を考えます。(プラグインが何だか分からないという方は、「アスキー デジタル用語辞典」や「IT用語辞典 e-Words」等をご覧ください。)

早速ですが、プラグイン機能の実現のために参考になりそうな記事を以下にいくつか紹介します。

これらの記事で紹介されている方法は基本的にはほとんど同じで、それは、インターフェイスを使用するという方法です。つまり、プラグインとして必要となる機能をプロパティやメソッド(さらに、イベントやインデクサ)として持つインターフェイスをあらかじめ用意しておき、このインターフェイスを実装したクラスとしてプラグインを作成するのです。

インターフェイスの作成

理屈は置いておき、実際にプラグインを作成してみましょう。

ここで作成するプラグインは、プラグインの名前を返す機能と、渡された文字列を処理して、結果を文字列として返す機能を有するものとし、それぞれの機能のためにNameプロパティとRunメソッドを持つインターフェイスを作ります。

Visual Studioでは、クラスライブラリのプロジェクトを作成し、次のようなコードを書き、ビルドします。.NET SDKの場合は、/target:libraryコンパイラオプションにより、コードライブラリを作成します。なお、ここで作成されたアセンブリファイル名は、"Plugin.dll"であるとします。

注意:Visual StudioのVB.NETの場合は、プロジェクトのプロパティの「ルート名前空間」が空白になっているものとします。デフォルトではプロジェクト名となっています。
VB.NET
コードを隠すコードを選択
Imports System

Namespace Plugin
    Public Interface IPlugin
        ReadOnly Property Name() As String
        Function Run(ByVal str As String) As String
    End Interface
End Namespace
C#
コードを隠すコードを選択
using System;

namespace Plugin
{
    public interface IPlugin
    {
        string Name {get;}
        string Run(string str);
    }
}

プラグインの作成

次に、このインターフェイスを基に、プラグインを作ります。プラグインもクラスライブラリとして作成します。プラグインは今作成したIPluginインターフェイスを実装する必要がありますので、"Plugin.dll"を参照に追加します。具体的には、Visual Studioの場合は、ソリューションエクスプローラの「参照設定」に"Plugin.dll"を追加します。.NET SDKの場合は、/referenceコンパイラオプションを使用します。

ここでは渡された文字列の文字数を返すプラグインを作成することにします。IPluginインターフェイスを実装した次のようなCountCharsクラスを作成します。このクラスライブラリは"CountChars.dll"という名前のアセンブリファイルとしてビルドするものとします。

VB.NET
コードを隠すコードを選択
Imports System

Namespace CountChars
    Public Class CountChars
        Implements Plugin.IPlugin

        Public ReadOnly Property Name() As String _
            Implements Plugin.IPlugin.Name
            Get
                Return "文字数取得"
            End Get
        End Property

        Public Function Run(ByVal str As String) As String _
            Implements Plugin.IPlugin.Run
            Return str.Length.ToString()
        End Function
    End Class
End Namespace
C#
コードを隠すコードを選択
using System;

namespace CountChars
{
    public class CountChars : Plugin.IPlugin
    {
        public string Name
        {
            get
            {
                return "文字数取得";
            }
        }
        public string Run(string str)
        {
            return str.Length.ToString();
        }
    }
}

プラグインを使うアプリケーションの作成

残るはプラグインを使用するメインアプリケーションの作成です。メインアプリケーションでは、インストールされている有効なプラグインをどのように探すか、そして、プラグインクラスのインスタンスをどのように作成するかということが問題となります。

まずインストールされているプラグインを探す方法についてです。プラグインは必ず".dll"という拡張子を持ち、指定されたフォルダに置かれていなければならないという約束にしておきます(ここでは、メインアプリケーションがインストールされているフォルダにある"plugins"フォルダにプラグインを置くものとします)。これにより、メインアプリケーションはプラグインフォルダにある".dll"の拡張子を持つファイルのみを調べればよいことになります。さらに、アセンブリにIPluginインターフェイスを実装したクラスがあるか調べるには、リフレクションを使用します。

またプラグインクラスのインスタンスを作成するには、Activator.CreateInstanceメソッドなどを使用すればよいでしょう(下のサンプルでは、Assembly.CreateInstanceメソッドを使用しています)。

それでは実際に作成してみましょう。ここではメインアプリはコンソールアプリケーションとして作成します。また、"Plugin.dll"を参照に追加します。

まずはプラグインを扱うクラス("PluginInfo")を作成します。このクラスは、プラグインのアセンブリファイルのパスとクラス名を返すプロパティ(それぞれ"Location"と"ClassName")と、有効なプラグインを探すメソッド("FindPlugins")、IPluginのインスタンスを作成するメソッド("CreateInstance")を持ちます。このクラスのコードを以下に示します。

VB.NET
コードを隠すコードを選択
Imports System

Namespace MainApplication
    ''' <summary>
    ''' プラグインに関する情報
    ''' </summary>
    Public Class PluginInfo
        Private _location As String
        Private _className As String

        ''' <summary>
        ''' PluginInfoクラスのコンストラクタ
        ''' </summary>
        ''' <param name="path">アセンブリファイルのパス</param>
        ''' <param name="cls">クラスの名前</param>
        Private Sub New(ByVal path As String, ByVal cls As String)
            Me._location = path
            Me._className = cls
        End Sub

        ''' <summary>
        ''' アセンブリファイルのパス
        ''' </summary>
        Public ReadOnly Property Location() As String
            Get
                Return _location
            End Get
        End Property

        ''' <summary>
        ''' クラスの名前
        ''' </summary>
        Public ReadOnly Property ClassName() As String
            Get
                Return _className
            End Get
        End Property

        ''' <summary>
        ''' 有効なプラグインを探す
        ''' </summary>
        ''' <returns>有効なプラグインのPluginInfo配列</returns>
        Public Shared Function FindPlugins() As PluginInfo()
            Dim plugins As New System.Collections.ArrayList
            'IPlugin型の名前
            Dim ipluginName As String = _
                GetType(Plugin.IPlugin).FullName

            'プラグインフォルダ
            Dim folder As String = _
                System.IO.Path.GetDirectoryName( _
                System.Reflection.Assembly. _
                GetExecutingAssembly().Location)
            folder += "\plugins"
            If Not System.IO.Directory.Exists(folder) Then
                Throw New ApplicationException( _
                    "プラグインフォルダ""" + folder + _
                    """が見つかりませんでした。")
            End If

            '.dllファイルを探す
            Dim dlls As String() = _
                System.IO.Directory.GetFiles(folder, "*.dll")

            Dim dll As String
            For Each dll In dlls
                Try
                    'アセンブリとして読み込む
                    Dim asm As System.Reflection.Assembly = _
                        System.Reflection.Assembly.LoadFrom(dll)
                    Dim t As Type
                    For Each t In asm.GetTypes()
                        'アセンブリ内のすべての型について、
                        'プラグインとして有効か調べる
                        If t.IsClass AndAlso t.IsPublic AndAlso _
                            Not t.IsAbstract AndAlso _
                            Not (t.GetInterface(ipluginName) _
                                Is Nothing) Then
                            'PluginInfoをコレクションに追加する
                            plugins.Add( _
                                New PluginInfo(dll, t.FullName))
                        End If
                    Next t
                Catch
                End Try
            Next dll

            'コレクションを配列にして返す
            Return CType(plugins.ToArray( _
                GetType(PluginInfo)), PluginInfo())
        End Function

        ''' <summary>
        ''' プラグインクラスのインスタンスを作成する
        ''' </summary>
        ''' <returns>プラグインクラスのインスタンス</returns>
        Public Function CreateInstance() As Plugin.IPlugin
            Try
                'アセンブリを読み込む
                Dim asm As System.Reflection.Assembly = _
                    System.Reflection.Assembly.LoadFrom(Me.Location)
                'クラス名からインスタンスを作成する
                Return CType(asm.CreateInstance(Me.ClassName), _
                    Plugin.IPlugin)
            Catch
            End Try
        End Function
    End Class
End Namespace
C#
コードを隠すコードを選択
using System;

namespace Plugin
{
    /// <summary>
    /// プラグインに関する情報
    /// </summary>
    public class PluginInfo
    {
        private string _location;
        private string _className;

        /// <summary>
        /// PluginInfoクラスのコンストラクタ
        /// </summary>
        /// <param name="path">アセンブリファイルのパス</param>
        /// <param name="cls">クラスの名前</param>
        private PluginInfo(string path, string cls)
        {
            this.Location = path;
            this.ClassName = cls;
        }

        /// <summary>
        /// アセンブリファイルのパス
        /// </summary>
        public string Location
        {
            get {return _location;}
        }

        /// <summary>
        /// クラスの名前
        /// </summary>
        public string ClassName
        {
            get {return _className;}
        }

        /// <summary>
        /// 有効なプラグインを探す
        /// </summary>
        /// <returns>有効なプラグインのPluginInfo配列</returns>
        public static PluginInfo[] FindPlugins()
        {
            System.Collections.ArrayList plugins =
                new System.Collections.ArrayList();
            //IPlugin型の名前
            string ipluginName = typeof(Plugin.IPlugin).FullName;

            //プラグインフォルダ
            string folder = System.IO.Path.GetDirectoryName(
                System.Reflection.Assembly
                .GetExecutingAssembly().Location);
            folder += "\\plugins";
            if (!System.IO.Directory.Exists(folder))
                throw new ApplicationException(
                    "プラグインフォルダ\"" + folder +
                    "\"が見つかりませんでした。");

            //.dllファイルを探す
            string[] dlls =
                System.IO.Directory.GetFiles(folder, "*.dll");

            foreach (string dll in dlls)
            {
                try
                {
                    //アセンブリとして読み込む
                    System.Reflection.Assembly asm =
                        System.Reflection.Assembly.LoadFrom(dll);
                    foreach (Type t in asm.GetTypes())
                    {
                        //アセンブリ内のすべての型について、
                        //プラグインとして有効か調べる
                        if (t.IsClass && t.IsPublic && !t.IsAbstract &&
                            t.GetInterface(ipluginName) != null)
                        {
                            //PluginInfoをコレクションに追加する
                            plugins.Add(
                                new PluginInfo(dll, t.FullName));
                        }
                    }
                }
                catch
                {
                }
            }

            //コレクションを配列にして返す
            return (PluginInfo[]) plugins.ToArray(typeof(PluginInfo));
        }

        /// <summary>
        /// プラグインクラスのインスタンスを作成する
        /// </summary>
        /// <returns>プラグインクラスのインスタンス</returns>
        public Plugin.IPlugin CreateInstance()
        {
            try
            {
                //アセンブリを読み込む
                System.Reflection.Assembly asm =
                    System.Reflection.Assembly.LoadFrom(this.Location);
                //クラス名からインスタンスを作成する
                return (Plugin.IPlugin)
                    asm.CreateInstance(this.ClassName);
            }
            catch
            {
                return null;
            }
        }
    }
}

FindPluginsメソッドが多少複雑ですので、簡単に説明しておきます。FindPluginsメソッドでは、Assembly.LoadFromメソッドでアセンブリを読み込んだ後、アセンブリ内のすべての型について、その型がクラスであり、パブリックであり、抽象クラスでないことを確認し、さらにType.GetInterfaceメソッドにより、IPluginインターフェイスを実装していることを確認します。これらすべての条件に当てはまった場合に有効なプラグインと判断します。(よってこの例では、1つのアセンブリファイルに複数のプラグインクラスを定義することができます。)

ここまで来たら、後は簡単です。PluginInfoクラスを使ったメインのクラス(エントリポイント)のサンプルは、次のようになります。

VB.NET
コードを隠すコードを選択
Imports System

Namespace MainApplication
    Public Class PluginHost
        '/ <summary>
        '/ エントリポイント
        '/ </summary>
        <STAThread()> _
        Public Shared Sub Main()
            'インストールされているプラグインを調べる
            Dim pis As PluginInfo() = PluginInfo.FindPlugins()

            'すべてのプラグインクラスのインスタンスを作成する
            Dim plugins(pis.Length - 1) As Plugin.IPlugin
            Dim i As Integer
            For i = 0 To plugins.Length - 1
                plugins(i) = pis(i).CreateInstance()
            Next i

            '有効なプラグインを表示し、使用するプラグインを選ばせる
            Dim number As Integer = -1
            Do
                For i = 0 To plugins.Length - 1
                    Console.WriteLine("{0}:{1}", i, plugins(i).Name)
                Next i
                Console.WriteLine( _
                    "使用するプラグインの番号を入力してください。:")
                Try
                    number = Integer.Parse(Console.ReadLine())
                Catch
                End Try
            Loop While number < 0 Or number >= pis.Length
            Console.WriteLine(plugins(number).Name + " を使用します。")

            'プラグインに渡す文字列の入力を促す
            Console.WriteLine("文字列を入力してください。:")
            Dim str As String = Console.ReadLine()

            'プラグインのRunメソッドを呼び出して結果を取得する
            Dim result As String = plugins(number).Run(str)

            '結果の表示
            Console.WriteLine("結果:")
            Console.WriteLine(result)

            Console.ReadLine()
        End Sub
    End Class
End Namespace
C#
コードを隠すコードを選択
using System;

namespace MainApplication
{
    public class PluginHost
    {
        /// <summary>
        /// エントリポイント
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            //インストールされているプラグインを調べる
            PluginInfo[] pis = Plugin.PluginInfo.FindPlugins();

            //すべてのプラグインクラスのインスタンスを作成する
            Plugin.IPlugin[] plugins = new Plugin.IPlugin[pis.Length];
            for (int i = 0; i < plugins.Length; i++)
                plugins[i] = pis[i].CreateInstance();

            //有効なプラグインを表示し、使用するプラグインを選ばせる
            int number = -1;
            do
            {
                for (int i = 0; i < plugins.Length; i++)
                    Console.WriteLine("{0}:{1}", i, plugins[i].Name);
                Console.WriteLine(
                    "使用するプラグインの番号を入力してください。:");
                try
                {
                    number = int.Parse(Console.ReadLine());
                }
                catch
                {
                    Console.WriteLine("数字を入力してください。");
                }
            } while (number < 0 || number >= pis.Length);
            Console.WriteLine(plugins[number].Name + " を使用します。");

            //プラグインに渡す文字列の入力を促す
            Console.WriteLine("文字列を入力してください。:");
            string str = Console.ReadLine();

            //プラグインのRunメソッドを呼び出して結果を取得する
            string result = plugins[number].Run(str);

            //結果の表示
            Console.WriteLine("結果:");
            Console.WriteLine(result);

            Console.ReadLine();
        }

    }
}

メインアプリ実行前に実行ファイルのあるフォルダに"plugins"というフォルダを作り、そこに先ほど作成した"CountChars.dll"をコピーしておいてください。これで、"CountChars.dll"をプラグインとして認識できます。

基本的なプラグイン機能の実現方法は以上です。しかしより高度なプラグイン機能が必要な場合には、これだけでは不十分かもしれません。例えば、プラグインからメインアプリにフィードバックしたいなど、プラグインからメインアプリケーションの機能(メソッド)を呼び出したいケースもあるでしょう。そのような場合には、プラグインを使う側で実装するインターフェイスを定義するという方法があります(実は一番初めに紹介した「プラグイン機能の実現のために参考になりそうな記事」のすべてでこの方法が使われています)。続いては、Windowsアプリケーションのサンプルとして、この方法を紹介します。

Windowsアプリケーションでプラグイン機能を実現する

ここからはさらに実用的な例として、Windowsアプリケーションでプラグイン機能を実現する方法を考えます。ここでは具体的に、プラグインの使用できる簡単なエディタを作成します。

あくまでプラグイン機能を説明することが目的ですので、エディタは思い切り単純にし、フォームにRichTextBoxとMainMenuのみを配置することにします。プラグインからエディタのRichTextBoxコントロールにアクセスすることにより、プラグインの機能が果たせるようにします。

インターフェイスを定義する

前回と同じように、まずプラグインのクラスが実装すべきインターフェイスを定義します。今回はプラグインのインターフェイスに加えて、プラグインを使用するホストの側が実装すべきインターフェイスも定義することにします。ホストのためのインターフェイスでは、プラグインのホストとして必要な機能をメンバとして定義し、プラグインからこのメンバを通してホストにアクセスできるようにします。

具体的には、プラグインから指定されたメッセージをホストで表示するためのメソッドと、ホストのRichTextBoxコントロールを取得するためのプロパティ、さらにホストのメインフォームを取得するためのプロパティを定義することにします。

それでは実際にこれらのインターフェイスを作成してみましょう。前と同様、クラスライブラリとして作成するため、Visual Studioではクラスライブラリのプロジェクトを作成し("Plugin"という名前で作成しています)、.NET SDKでは/target:libraryコンパイラオプションを使用します。また、アセンブリファイル名は、"Plugin.dll"とします。さらに今回はWindowsアプリケーションを扱うため、"System.Windows.Forms.dll"を参照に追加します。(参照に追加するには、Visual Studio .NETの場合は、ソリューションエクスプローラの「参照設定」を、.NET SDKの場合は、/referenceコンパイラオプションを使用します。)

コードは、次のようになります。IPluginインターフェイスがプラグインのためのインターフェイスで、IPluginHostインターフェイスがプラグインのホストのためのインターフェイスです。

VB.NET
コードを隠すコードを選択
Imports System
Imports System.Windows.Forms

Namespace Plugin
    ''' <summary>
    ''' プラグインで実装するインターフェイス
    ''' </summary>
    Public Interface IPlugin
        ''' <summary>
        ''' プラグインの名前
        ''' </summary>
        ReadOnly Property Name() As String

        ''' <summary>
        ''' プラグインのバージョン
        ''' </summary>
        ReadOnly Property Version() As String

        ''' <summary>
        ''' プラグインの説明
        ''' </summary>
        ReadOnly Property Description() As String

        ''' <summary>
        ''' プラグインのホスト
        ''' </summary>
        Property Host() As IPluginHost

        ''' <summary>
        ''' プラグインを実行する
        ''' </summary>
        Sub Run()
    End Interface

    ''' <summary>
    ''' プラグインのホストで実装するインターフェイス
    ''' </summary>
    Public Interface IPluginHost
        ''' <summary>
        ''' ホストのメインフォーム
        ''' </summary>
        ReadOnly Property MainForm() As Form

        ''' <summary>
        ''' ホストのRichTextBoxコントロール
        ''' </summary>
        ReadOnly Property RichTextBox() As RichTextBox

        ''' <summary>
        ''' ホストでメッセージを表示する
        ''' </summary>
        ''' <param name="plugin">メソッドを呼び出すプラグイン</param>
        ''' <param name="msg">表示するメッセージ</param>
        Sub ShowMessage(ByVal plugin As IPlugin, ByVal msg As String)
    End Interface
End Namespace
C#
コードを隠すコードを選択
using System;
using System.Windows.Forms;

namespace Plugin
{
    /// <summary>
    /// プラグインで実装するインターフェイス
    /// </summary>
    public interface IPlugin
    {
        /// <summary>
        /// プラグインの名前
        /// </summary>
        string Name {get;}

        /// <summary>
        /// プラグインのバージョン
        /// </summary>
        string Version {get;}

        /// <summary>
        /// プラグインの説明
        /// </summary>
        string Description {get;}

        /// <summary>
        /// プラグインのホスト
        /// </summary>
        IPluginHost Host {get; set;}

        /// <summary>
        /// プラグインを実行する
        /// </summary>
        void Run();
    }

    /// <summary>
    /// プラグインのホストで実装するインターフェイス
    /// </summary>
    public interface IPluginHost
    {
        /// <summary>
        /// ホストのメインフォーム
        /// </summary>
        Form MainForm {get;}

        /// <summary>
        /// ホストのRichTextBoxコントロール
        /// </summary>
        RichTextBox RichTextBox {get;}

        /// <summary>
        /// ホストでメッセージを表示する
        /// </summary>
        /// <param name="plugin">メソッドを呼び出すプラグイン</param>
        /// <param name="msg">表示するメッセージ</param>
        void ShowMessage(IPlugin plugin, string msg);
    }
}

IPluginは前回と比べ、プラグインのバージョンと説明を取得するためのプロパティが新たに追加され、さらに、Runメソッドもパラメータ、返り値がなくなりました。また、IPluginHostを設定、取得するためのプロパティも加えられています。

プラグインを作成する

次に、IPluginインターフェイスを実装したプラグインのクラスを作成します。ここでは、RichTextBox内の文字数を表示するプラグイン(CountCharsクラス)を作成してみましょう。

プラグインも前号と同様に、クラスライブラリとして作成し、"Plugin.dll"を参照に追加します。また、"System.Windows.Forms.dll"も参照に追加してください。出力するアセンブリファイル名は、"CountChars.dll"とします。(Visual Studio .NETのVB.NETの場合は、プロジェクトのプロパティの「ルート名前空間」が空白になっているものとします。デフォルトではプロジェクト名となっていますので、変更する必要があります。)

CountCharsクラスのコードは次のようになります。

VB.NET
コードを隠すコードを選択
Imports System

Namespace CountChars
    ''' <summary>
    ''' 文字数を表示するためのプラグイン
    ''' </summary>
    Public Class CountChars
        Implements Plugin.IPlugin

        Private _host As Plugin.IPluginHost

        'IPluginのメンバ
        Public ReadOnly Property Name() As String _
            Implements Plugin.IPlugin.Name
            Get
                Return "文字数取得"
            End Get
        End Property

        Public ReadOnly Property Version() As String _
            Implements Plugin.IPlugin.Version
            Get
                '自分自身のAssemblyを取得し、バージョンを返す
                Dim asm As System.Reflection.Assembly = _
                    System.Reflection.Assembly.GetExecutingAssembly()
                Dim ver As System.Version = asm.GetName().Version
                Return ver.ToString()
            End Get
        End Property

        Public ReadOnly Property Description() As String _
            Implements Plugin.IPlugin.Description
            Get
                Return "エディタで編集中の文章の文字数を表示します。"
            End Get
        End Property

        Public Property Host() As Plugin.IPluginHost _
            Implements Plugin.IPlugin.Host
            Get
                Return Me._host
            End Get
            Set(ByVal Value As Plugin.IPluginHost)
                Me._host = Value
            End Set
        End Property

        ''' <summary>
        ''' RichTextBoxの文字数を表示する
        ''' </summary>
        Public Sub Run() Implements Plugin.IPlugin.Run
            Dim msg As String = String.Format("文字数 : {0} 文字", _
                Me._host.RichTextBox.Text.Length)
            Me._host.ShowMessage(Me, msg)
        End Sub
    End Class
End Namespace
C#
コードを隠すコードを選択
using System;

namespace CountChars
{
    /// <summary>
    /// 文字数を表示するためのプラグイン
    /// </summary>
    public class CountChars : Plugin.IPlugin
    {
        private Plugin.IPluginHost _host;

        //IPluginのメンバ
        public string Name
        {
            get
            {
                return "文字数取得";
            }
        }
        public string Version
        {
            get
            {
                //自分自身のAssemblyを取得し、バージョンを返す
                System.Reflection.Assembly asm = 
                    System.Reflection.Assembly.GetExecutingAssembly();
                System.Version ver = asm.GetName().Version;
                return ver.ToString();
            }
        }
        public string Description
        {
            get
            {
                return "エディタで編集中の文章の文字数を表示します。";
            }
        }
        public Plugin.IPluginHost Host
        {
            get
            {
                return this._host;
            }
            set
            {
                this._host = value;
            }
        }
        /// <summary>
        /// RichTextBoxの文字数を表示する
        /// </summary>
        public void Run()
        {
            string msg = 
                string.Format("文字数 : {0} 文字", 
                    this._host.RichTextBox.Text.Length);
            this._host.ShowMessage(this, msg);
        }
    }
}

特に説明を必要とする箇所はないでしょう。Runメソッドでは、IPluginHostオブジェクトからRichTextBoxにアクセスし、文字数を取得し、IPluginHostのShowMessageメソッドで結果をホストで表示しています。

ウィンドウを表示するプラグインの作成

次にWindowsアプリケーションらしく、ウィンドウを表示するプラグインを作成してみましょう。ここでは、「検索」ウィンドウにより、RichTextBoxから指定された文字列を検索するプラグインを作ります(エディタとしては、プラグインで処理する機能ではありませんが)。

まず、CountCharsクラスと同様、クラスライブラリのプロジェクトを作成し(名前は、"FindString"とします)、"Plugin.dll"と"System.Windows.Forms.dll"を参照に追加します("System.Windows.Forms.dll"は今追加しなくても、「Windowsフォームの追加」でプロジェクトにフォームを追加すれば、自動的に追加されます)。

続いて、「Windowsフォームの追加」でプロジェクトにフォーム(FindForm)を追加し、「検索」ウィンドウを作成します。このフォームに配置するコントロール及び、変更するプロパティ(あるいはイベント)の一覧は次のようになります。

Form
Name FindForm
Text 検索
AcceptButton findButton
CancelButton closeButton
FormBorderStyle FixedToolWindow
ShowInTaskbar false
TextBoxコントロール
Name findString
Text ""
Buttonコントロール
Name findButton
Text 次を検索
DialogResult OK
Clickイベント findButton_Click
Buttonコントロール
Name closeButton
Text 閉じる
DialogResult Cancel
Clickイベント closeButton_Click
CheckBoxコントロール
Name wholeWord
Text 単語単位で検索する
CheckBoxコントロール
Name matchCase
Text 大文字小文字を区別する
CheckBoxコントロール
Name reverse
Text 上へ検索する

さらに、FindFormクラスに次のコードを追加し、指定されたRichTextBoxを検索できるようにします。

VB.NET
コードを隠すコードを選択
'Imports System.Windows.Forms
'がソースファイルの一番上に書かれているものとする

'検索するRichTextBox
Friend RichTextBox As RichTextBox

Private Sub findButton_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles findButton.Click
    '検索オプションを決定する
    Dim finds As RichTextBoxFinds = RichTextBoxFinds.None
    If Me.wholeWord.Checked Then
        finds = finds Or RichTextBoxFinds.WholeWord
    End If
    If Me.matchCase.Checked Then
        finds = finds Or RichTextBoxFinds.MatchCase
    End If
    If Me.reverse.Checked Then
        finds = finds Or RichTextBoxFinds.Reverse
    End If

    '検索範囲を決定する
    Dim startPos, endPos As Integer
    If Not Me.reverse.Checked Then
        startPos = Me.RichTextBox.SelectionStart + _
            Me.RichTextBox.SelectionLength
        endPos = -1
        If startPos >= Me.RichTextBox.TextLength Then
            MessageBox.Show("検索が完了しました。", "検索")
            Return
        End If
    Else
        startPos = 0
        endPos = Me.RichTextBox.SelectionStart
        If endPos <= 0 Then
            MessageBox.Show("検索が完了しました。", "検索")
            Return
        End If
    End If

    '検索する
    If Me.RichTextBox.Find( _
        findString.Text, startPos, endPos, finds) < 0 Then
        MessageBox.Show("検索が完了しました。", "検索")
    Else
        Me.RichTextBox.Focus()
    End If
End Sub

Private Sub closeButton_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles closeButton.Click
    Me.Close()
End Sub
C#
コードを隠すコードを選択
//using System.Windows.Forms;
//がソースファイルの一番上に書かれているものとする

//検索するRichTextBox
internal RichTextBox RichTextBox;

//「次を検索」がクリックされた時
private void findButton_Click(
    object sender, System.EventArgs e)
{
    //検索オプションを決定する
    RichTextBoxFinds finds = RichTextBoxFinds.None;
    if (this.wholeWord.Checked)
        finds |= RichTextBoxFinds.WholeWord;
    if (this.matchCase.Checked)
        finds |= RichTextBoxFinds.MatchCase;
    if (this.reverse.Checked)
        finds |= RichTextBoxFinds.Reverse;

    //検索範囲を決定する
    int startPos, endPos;
    if (!this.reverse.Checked)
    {
        startPos = this.RichTextBox.SelectionStart + 
            this.RichTextBox.SelectionLength;
        endPos = -1;
        if (startPos >= this.RichTextBox.TextLength)
        {
            MessageBox.Show("検索が完了しました。", "検索");
            return;
        }
    }
    else
    {
        startPos = 0;
        endPos = this.RichTextBox.SelectionStart;
        if (endPos <= 0)
        {
            MessageBox.Show("検索が完了しました。", "検索");
            return;
        }
    }

    //検索する
    if (this.RichTextBox.Find(
        findString.Text, startPos, endPos, finds) < 0)
        MessageBox.Show("検索が完了しました。", "検索");
    else
        this.RichTextBox.Focus();
}

//「閉じる」がクリックされた時
private void closeButton_Click(
    object sender, System.EventArgs e)
{
    this.Close();
}

次にこのフォームを表示させるプラグインクラス(FindString)を作成します。コードは、次のようになります。

VB.NET
コードを隠すコードを選択
Imports System
Imports System.Windows.Forms

Namespace FindString
    Public Class FindString
        Implements Plugin.IPlugin

        Private _host As Plugin.IPluginHost
        Private _mainForm As FindForm

        'IPluginのメンバ
        Public ReadOnly Property Name() As String _
            Implements Plugin.IPlugin.Name
            Get
                Return "文字列の検索"
            End Get
        End Property

        Public ReadOnly Property Description() As String _
            Implements Plugin.IPlugin.Description
            Get
                Return "編集中の文章から指定された文字列を検索します。"
            End Get
        End Property

        Public Property Host() As Plugin.IPluginHost _
            Implements Plugin.IPlugin.Host
            Get
                Return _host
            End Get
            Set(ByVal Value As Plugin.IPluginHost)
                _host = Value
            End Set
        End Property

        Public ReadOnly Property Version() As String _
            Implements Plugin.IPlugin.Version
            Get
                '自分自身のAssemblyを取得し、バージョンを返す
                Dim asm As System.Reflection.Assembly = _
                    System.Reflection.Assembly.GetExecutingAssembly()
                Dim ver As System.Version = asm.GetName().Version
                Return ver.ToString()
            End Get
        End Property

        Public Sub Run() Implements Plugin.IPlugin.Run
            'フォームが表示されていれば、アクティブにして終了
            If Not (Me._mainForm Is Nothing) AndAlso _
                Not Me._mainForm.IsDisposed Then
                Me._mainForm.Activate()
                Return
            End If

            '検索ウィンドウを作成し、表示する
            Me._mainForm = New FindForm
            Me._mainForm.RichTextBox = Me._host.RichTextBox
            Me._mainForm.Owner = Me._host.MainForm
            Me._mainForm.Show()
        End Sub
    End Class
End Namespace
C#
コードを隠すコードを選択
using System;
using System.Windows.Forms;

namespace FindString
{
    public class FindString : Plugin.IPlugin
    {
        private Plugin.IPluginHost _host;
        private FindForm _mainForm;

        //IPluginのメンバ
        public string Name
        {
            get
            {
                return "文字列の検索";
            }
        }
        public string Description
        {
            get
            {
                return "編集中の文章から指定された文字列を検索します。";
            }
        }
        public Plugin.IPluginHost Host
        {
            get
            {
                return _host;
            }
            set
            {
                _host = value;
            }
        }
        public string Version
        {
            get
            {
                //自分自身のAssemblyを取得し、バージョンを返す
                System.Reflection.Assembly asm = 
                    System.Reflection.Assembly.GetExecutingAssembly();
                System.Version ver = asm.GetName().Version;
                return ver.ToString();
            }
        }

        public void Run()
        {
            //フォームが表示されていれば、アクティブにして終了
            if (this._mainForm != null && !this._mainForm.IsDisposed)
            {
                this._mainForm.Activate();
                return;
            }

            //検索ウィンドウを作成し、表示する
            this._mainForm = new FindForm();
            this._mainForm.RichTextBox = this._host.RichTextBox;
            this._mainForm.Owner = this._host.MainForm;
            this._mainForm.Show();
        }
    }
}

RunメソッドでFindFormを表示しているだけで、特に問題はないでしょう。(表示する前にRichTextBoxの設定と、オーナーウィンドウの設定を行っています。)

メインアプリケーションの作成

ようやくここまでたどり着きました。いよいよプラグインを使用するホストのアプリケーションを作成します。

ホストアプリケーションは、Windowsアプリケーションプロジェクトとして作成し(名前は、"MainApplication"とします)、"Plugin.dll"を参照に追加します。

フォームには、RichTextBoxと、メニュー、さらにメッセージを表示するためのStatusBarコントロールを配置します。変更するフォームのプロパティと、フォームに配置するコントロールとそのプロパティ(そしてイベント)の一覧を以下に示します。

Form
Name Form1
Menu mainMenu
Loadイベント Form1_Load
RichTextBoxコントロール
Name mainRichTextBox
Dock Fill
StatusBarコントロール
Name mainStatusbar
MainMenuコントロール
Name mainMenu
MenuItemコントロール
Name menuPlugins
Text プラグイン(&P)
Clickイベント menuPlugin_Click
MenuItemコントロール
Name menuHelp
Text ヘルプ(&H)
MenuItemコントロール
Name menuAbout
Text バージョン情報(&A)...
Clickイベント menuAbout_Click

次に前号で作成したPluginInfoクラスをプロジェクトに追加します。ただし、CreateInstanceメソッドでIPluginHostオブジェクトをIPluginHost.Hostプロパティに設定するように変更しています。

PluginInfoクラスは次のようなコードです。

VB.NET
コードを隠すコードを選択
Imports System

''' <summary>
''' プラグインに関する情報
''' </summary>
Public Class PluginInfo
    Private _location As String
    Private _className As String

    ''' <summary>
    ''' PluginInfoクラスのコンストラクタ
    ''' </summary>
    ''' <param name="path">アセンブリファイルのパス</param>
    ''' <param name="cls">クラスの名前</param>
    Private Sub New(ByVal path As String, ByVal cls As String)
        Me._location = path
        Me._className = cls
    End Sub

    ''' <summary>
    ''' アセンブリファイルのパス
    ''' </summary>
    Public ReadOnly Property Location() As String
        Get
            Return _location
        End Get
    End Property

    ''' <summary>
    ''' クラスの名前
    ''' </summary>
    Public ReadOnly Property ClassName() As String
        Get
            Return _className
        End Get
    End Property

    ''' <summary>
    ''' 有効なプラグインを探す
    ''' </summary>
    ''' <returns>有効なプラグインのPluginInfo配列</returns>
    Public Shared Function FindPlugins() As PluginInfo()
        Dim plugins As New System.Collections.ArrayList
        'IPlugin型の名前
        Dim ipluginName As String = _
            GetType(Plugin.IPlugin).FullName

        'プラグインフォルダ
        Dim folder As String = _
            System.IO.Path.GetDirectoryName( _
            System.Reflection.Assembly. _
            GetExecutingAssembly().Location)
        folder += "\plugins"
        If Not System.IO.Directory.Exists(folder) Then
            Throw New ApplicationException( _
                "プラグインフォルダ""" + folder + _
                """が見つかりませんでした。")
        End If

        '.dllファイルを探す
        Dim dlls As String() = _
            System.IO.Directory.GetFiles(folder, "*.dll")

        Dim dll As String
        For Each dll In dlls
            Try
                'アセンブリとして読み込む
                Dim asm As System.Reflection.Assembly = _
                    System.Reflection.Assembly.LoadFrom(dll)
                Dim t As Type
                For Each t In asm.GetTypes()
                    'アセンブリ内のすべての型について、
                    'プラグインとして有効か調べる
                    If t.IsClass And t.IsPublic And _
                        Not t.IsAbstract And _
                        Not (t.GetInterface(ipluginName) Is Nothing _
                        ) Then
                        'PluginInfoをコレクションに追加する
                        plugins.Add(New PluginInfo(dll, t.FullName))
                    End If
                Next t
            Catch
            End Try
        Next dll

        'コレクションを配列にして返す
        Return CType(plugins.ToArray( _
            GetType(PluginInfo)), PluginInfo())
    End Function 'FindPlugins

    ''' <summary>
    ''' プラグインクラスのインスタンスを作成する
    ''' </summary>
    ''' <returns>プラグインクラスのインスタンス</returns>
    Public Function CreateInstance( _
        ByVal host As Plugin.IPluginHost) As Plugin.IPlugin
        Try
            'アセンブリを読み込む
            Dim asm As System.Reflection.Assembly = _
                System.Reflection.Assembly.LoadFrom(Me.Location)
            'クラス名からインスタンスを作成する
            Dim plugin As Plugin.IPlugin = _
                CType(asm.CreateInstance(Me.ClassName), _
                    Plugin.IPlugin)
            'IPluginHostの設定
            plugin.Host = host
            Return plugin
        Catch
        End Try
    End Function
End Class
C#
コードを隠すコードを選択
using System;

namespace MainApplication
{
    /// <summary>
    /// プラグインに関する情報
    /// </summary>
    public class PluginInfo
    {
        private string _location;
        private string _className;

        /// <summary>
        /// PluginInfoクラスのコンストラクタ
        /// </summary>
        /// <param name="path">アセンブリファイルのパス</param>
        /// <param name="cls">クラスの名前</param>
        private PluginInfo(string path, string cls)
        {
            this._location = path;
            this._className = cls;
        }

        /// <summary>
        /// アセンブリファイルのパス
        /// </summary>
        public string Location
        {
            get {return _location;}
        }

        /// <summary>
        /// クラスの名前
        /// </summary>
        public string ClassName
        {
            get {return _className;}
        }

        /// <summary>
        /// 有効なプラグインを探す
        /// </summary>
        /// <returns>有効なプラグインのPluginInfo配列</returns>
        public static PluginInfo[] FindPlugins()
        {
            System.Collections.ArrayList plugins =
                new System.Collections.ArrayList();
            //IPlugin型の名前
            string ipluginName = typeof(Plugin.IPlugin).FullName;

            //プラグインフォルダ
            string folder = System.IO.Path.GetDirectoryName(
                System.Reflection.Assembly
                .GetExecutingAssembly().Location);
            folder += "\\plugins";
            if (!System.IO.Directory.Exists(folder))
                throw new ApplicationException(
                    "プラグインフォルダ\"" + folder +
                    "\"が見つかりませんでした。");

            //.dllファイルを探す
            string[] dlls =
                System.IO.Directory.GetFiles(folder, "*.dll");

            foreach (string dll in dlls)
            {
                try
                {
                    //アセンブリとして読み込む
                    System.Reflection.Assembly asm =
                        System.Reflection.Assembly.LoadFrom(dll);
                    foreach (Type t in asm.GetTypes())
                    {
                        //アセンブリ内のすべての型について、
                        //プラグインとして有効か調べる
                        if (t.IsClass && t.IsPublic && !t.IsAbstract &&
                            t.GetInterface(ipluginName) != null)
                        {
                            //PluginInfoをコレクションに追加する
                            plugins.Add(
                                new PluginInfo(dll, t.FullName));
                        }
                    }
                }
                catch
                {
                }
            }

            //コレクションを配列にして返す
            return (PluginInfo[]) plugins.ToArray(typeof(PluginInfo));
        }

        /// <summary>
        /// プラグインクラスのインスタンスを作成する
        /// </summary>
        /// <returns>プラグインクラスのインスタンス</returns>
        public Plugin.IPlugin CreateInstance(Plugin.IPluginHost host)
        {
            try
            {
                //アセンブリを読み込む
                System.Reflection.Assembly asm =
                    System.Reflection.Assembly.LoadFrom(this.Location);
                //クラス名からインスタンスを作成する
                Plugin.IPlugin plugin =
                    (Plugin.IPlugin) asm.CreateInstance(this.ClassName);
                //IPluginHostの設定
                plugin.Host = host;
                return plugin;
            }
            catch
            {
                return null;
            }
        }
    }
}

IPluginHostインターフェイスは、フォームクラスで実装します。また、プラグインの読み込みと、メニューへの表示はフォームのLoadイベントハンドラで行い、メニューを選択することにより、プラグインを実行できるようにします。

以下に変更を加えたフォームクラスの主要部分のコード(Windowsフォームデザイナが作成したコードを除く)を示します。

VB.NET
コードを隠すコードを選択
Public Class Form1
    Inherits System.Windows.Forms.Form
    Implements Plugin.IPluginHost

    '(省略)

    'IPluginの配列
    Dim plugins() As Plugin.IPlugin

    'IPluginHostの実装
    Public ReadOnly Property MainForm() As Form _
        Implements Plugin.IPluginHost.MainForm
        Get
            Return Me
        End Get
    End Property

    Public ReadOnly Property RichTextBox() As RichTextBox _
        Implements Plugin.IPluginHost.RichTextBox
        Get
            Return mainRichTextBox
        End Get
    End Property

    Public Sub ShowMessage(ByVal plugin As Plugin.IPlugin, _
        ByVal msg As String) _
        Implements Plugin.IPluginHost.ShowMessage
        'ステータスバーに表示する
        mainStatusbar.Text = msg
    End Sub

    'メインフォームのLoadイベントハンドラ
    Private Sub Form1_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
        'インストールされているプラグインを探す
        Dim pis As PluginInfo() = PluginInfo.FindPlugins()

        'プラグインのインスタンスを取得する
        Me.plugins = New Plugin.IPlugin(pis.Length - 1) {}
        Dim i As Integer
        For i = 0 To (Me.plugins.Length) - 1
            Me.plugins(i) = pis(i).CreateInstance(Me)
        Next i

        'プラグインを実行するメニューを追加する
        Dim plugin As Plugin.IPlugin
        For Each plugin In Me.plugins
            Dim mi As New MenuItem(plugin.Name, _
                AddressOf menuPlugin_Click)
            Me.menuPlugins.MenuItems.Add(mi)
        Next plugin
    End Sub

    'プラグインのメニューがクリックされた時
    Private Sub menuPlugin_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles menuPlugins.Click
        Dim mi As MenuItem = CType(sender, MenuItem)
        'クリックされたプラグインを探す
        '(同じ名前のプラグインが複数あると困ったことに...)
        Dim plugin As Plugin.IPlugin
        For Each plugin In Me.plugins
            If mi.Text = plugin.Name Then
                'クリックされたプラグインを実行する
                plugin.Run()
                Return
            End If
        Next plugin
    End Sub

    'プラグインのバージョンを表示する
    Private Sub menuAbout_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles menuAbout.Click
        Dim msg As String = "インストールされているプラグイン" + vbLf _
            + "(名前 : 説明 : バージョン)" + vbLf + vbLf
        Dim plugin As Plugin.IPlugin
        For Each plugin In Me.plugins
            msg += String.Format("{0} : {1} : {2}" + vbLf, _
                plugin.Name, plugin.Description, plugin.Version)
        Next plugin
        MessageBox.Show(msg)
    End Sub
End Class
C#
コードを隠すコードを選択
namespace MainApplication
{
    /// <summary>
    /// Form1 の概要の説明です。
    /// </summary>
    public class Form1 : System.Windows.Forms.Form, Plugin.IPluginHost
    {
        //(省略)

        //IPluginの配列
        Plugin.IPlugin[] plugins;

        //IPluginHostの実装
        public Form MainForm
        {
            get
            {
                return (Form) this;
            }
        }
        public RichTextBox RichTextBox
        {
            get
            {
                return mainRichTextBox;
            }
        }
        public void ShowMessage(Plugin.IPlugin plugin, string msg)
        {
            //ステータスバーに表示する
            mainStatusbar.Text = msg;
        }

        //メインフォームのLoadイベントハンドラ
        private void Form1_Load(object sender, System.EventArgs e)
        {
            //インストールされているプラグインを探す
            PluginInfo[] pis = PluginInfo.FindPlugins();

            //プラグインのインスタンスを取得する
            this.plugins = new Plugin.IPlugin[pis.Length];
            for (int i = 0; i < this.plugins.Length; i++)
                this.plugins[i] = pis[i].CreateInstance(this);

            //プラグインを実行するメニューを追加する
            foreach (Plugin.IPlugin plugin in this.plugins)
            {
                MenuItem mi = new MenuItem(plugin.Name, 
                    new EventHandler(menuPlugin_Click));
                this.menuPlugins.MenuItems.Add(mi);
            }
        }

        //プラグインのメニューがクリックされた時
        private void menuPlugin_Click(
            object sender, System.EventArgs e)
        {
            MenuItem mi = (MenuItem) sender;
            //クリックされたプラグインを探す
            //(同じ名前のプラグインが複数あると困ったことに...)
            foreach (Plugin.IPlugin plugin in this.plugins)
            {
                if (mi.Text == plugin.Name)
                {
                    //クリックされたプラグインを実行する
                    plugin.Run();
                    return;
                }
            }
        }

        //プラグインのバージョンを表示する
        private void menuAbout_Click(
            object sender, System.EventArgs e)
        {
            string msg = "インストールされているプラグイン\n"
                + "(名前 : 説明 : バージョン)\n\n";
            foreach (Plugin.IPlugin plugin in this.plugins)
            {
                msg += string.Format("{0} : {1} : {2}\n",
                    plugin.Name, plugin.Description, plugin.Version);
            }
            MessageBox.Show(msg);
        }
    }
}

このメインアプリケーションを実行させるには、実行ファイルのあるフォルダに"plugins"というフォルダを作り、そこに前に作成したプラグイン"CountChars.dll"と"FindString.dll"をコピーしてください。うまくいくと、「プラグイン」メニューに「文字数取得」と「文字列の検索」が追加され、プラグインの機能を呼び出すことができるようになります。

以上でプラグイン機能を実現させる方法に関する解説はおしまいです。ここで紹介した知識を応用することにより、より複雑なプラグインも作成できるでしょう。この記事を読んで、プラグインを使ったアプリケーションを作ってみようと思われる方が一人でもいらっしゃるならばうれしいのですが。

  • 履歴:
  • 2009/11/9 「メインアプリケーションの作成」の表を修正。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • このサイトで紹介されているコードの多くは、例外処理が省略されています。例外処理については、こちらをご覧ください。
  • イベントハンドラの意味が分からない、C#のコードをそのまま書いても動かないという方は、こちらをご覧ください。
  • コードの先頭に記述されている「Imports ??? がソースファイルの一番上に書かれているものとする」(C#では、「using ???; がソースファイルの一番上に書かれているものとする」)の意味が分からないという方は、こちらをご覧ください。
  • 「???を参照に追加します」の意味が分からないという方は、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。