Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

.NETによるプラグイン機能を持つテキストエディタの作成

リフレクションとインターフェイスを用いたプラグイン機能の実現方法

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/04/01 00:00

「プラグイン」と呼ばれるプログラムの機能拡張が可能なアプリケーションの作り方を、プラグイン対応のテキストエディタを例に解説。

はじめに

 Adobe PhotoshopBecky! Internet Mailなどのアプリケーションでは「プラグイン」(または、「アドイン」、「エクステンション」等)と呼ばれるプログラムをインストールすることにより、機能を拡張することができます。この記事ではこのようなプラグイン機能を持ったアプリケーションの作り方を、プラグイン対応のテキストエディタを作成することにより、説明します。

 ここで紹介するプラグイン機能は、Becky!のように、プラグイン本体であるDLLファイルを指定されたフォルダにコピーすることにより、プラグインを使用するアプリケーション(ホスト)が自動的にプラグインを認識するというものです。

プラグイン機能のあるテキストエディタ
プラグイン機能のあるテキストエディタ

 なお、プラグイン機能の解説が目的のため、テキストエディタはフォームにRichTextBoxを貼り付けただけの貧弱なものですので、テキストエディタ作成の参考にはなりません。

対象読者

 .NETプログラミングを行ったことがある、もしくは、.NETプログラミングに興味のある方を対象としています。基本的な事柄については説明していませんので、不明な点はMSDNライブラリなどで調べてください。リフレクションに関しては、DOBON.NET .NET Tipsでも説明していますので、参考にしてください。

必要な環境

 サンプルはVisual Studio .NET 2003で作成され、.NET Framework 1.1で動作確認をしていますが、.NET Framework 1.0でも問題ないはずです。

プラグイン機能を実現するための基本的な考え方

 通常DLLアセンブリファイルを別のアセンブリから使用するには、あらかじめそのアセンブリを参照に追加しておく必要があります(Visual Studio .NETの場合は[プロジェクト]→[参照の追加...]を実行し、ダイアログから追加できます。.NET SDKの場合はコンパイルオプション「/reference」を使います)。しかしプラグインの場合はコンパイル時に参照することができないため、実行時に読み込む必要があります。これを可能にするのが、「リフレクション」です。

 リフレクションを利用すれば、実行時にDLL内の型のメンバにアクセスすることができます。よって、プラグインの機能をホストから呼び出す時のメソッド名、パラメータ、戻り値などを「約束事」として決めておけば、ホストからプラグインの機能を呼び出すことができるようになります。

 以上のような方法でプラグイン機能の実現は十分可能です。しかし、ホストとプラグインの間で決められた「約束事」があいまいでは、分かりづらく、危険です。この「約束」を確実にするためには、インターフェイスを用いるのがよいでしょう。つまり、プラグインが必ず持つべきメソッドやプロパティをインターフェイスで定義しておき、プラグインはこのインターフェイスを実装したものでなければならないとするのです。この様にしておけば、プラグインを作成する側では何をしなければならないのか明確になり、ホストの側ではプラグインを指定したインターフェイスの型として扱うことができます。

 同様にホストで実装すべきインターフェイスを定義しておくことにより、プラグインからホストの機能を呼び出したり、結果をコールバックすることが容易になります。

インターフェイスの作成

 まずは、インターフェイスを作成します。Visual Studio .NETでは、クラスライブラリのプロジェクトを作成し、プラグインで実装すべきインターフェイス(IPlugin)と、ホストで実装すべきインターフェイス(IPluginHost)を定義します。.NET SDKの場合は、「/target:library」オプションを使用してコンパイルします。サンプルでは、プロジェクト「Plugin」でプラグインとホストのインターフェイスを定義し、「Plugin.dll」というファイル名で出力しています。以下にそのコードの一部を抜粋します。

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

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

        /// <summary>
        /// プラグインのインスタンス作成直後に呼び出されるメソッド
        /// </summary>
        /// <param name="host">プラグインのホスト</param>
        void Initialize(IPluginHost host);

        //(以下省略)
    }

    /// <summary>
    /// プラグインのホストで実装するインターフェイス
    /// </summary>
    public interface IPluginHost
    {
        /// <summary>
        /// ホストのRichTextBoxコントロール
        /// </summary>
        RichTextBox RichTextBox {get;}

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

        //(以下省略)
    }
}
「IPlugin.vb」 (VB.NET)
Namespace Plugin
    ''' <summary>
    ''' プラグインで実装するインターフェイス
    ''' </summary>
    Public Interface IPlugin
        ''' <summary>
        ''' プラグインの名前
        ''' </summary>
        ReadOnly Property Name() As String

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

        ''' <summary>
        ''' プラグインのインスタンス作成直後に呼び出されるメソッド
        ''' </summary>
        ''' <param name="host">プラグインのホスト</param>
        Sub Initialize(ByVal host As IPluginHost)

        '(以下省略)
    End Interface

    ''' <summary>
    ''' プラグインのホストで実装するインターフェイス
    ''' </summary>
    Public Interface IPluginHost
        ''' <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

プラグインの作成

 次に、IPluginインターフェイスを実装することにより、プラグインを作成します。先程と同様にクラスライブラリのプロジェクトを新規作成し、「Plugin.dll」を参照に加え、IPluginを実装したクラスを作成します。サンプルには3つのプラグインのプロジェクト(「CountChar」、「FindString」、「FileHistory」)が含まれています。

 このうち最も単純な、文章の文字数を数えるだけのプラグインであるCountCharプラグインのコードの一部を以下に示します。

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

        public string Name
        {
            get
            {
                return "文字数取得";
            }
        }

        public void Initialize(Plugin.IPluginHost host)
        {
            this._host = host;
        }

        //(以下省略)
    }
}
「CountChars.vb」 (VB.NET)
Namespace CountChars
    ''' <summary>
    ''' 文字数を表示するためのプラグイン
    ''' </summary>
    Public Class CountChars
        Implements Plugin.IPlugin

        Private _host As Plugin.IPluginHost

        Public ReadOnly Property Name() As String _
            Implements Plugin.IPlugin.Name

            Get
                Return "文字数取得"
            End Get
        End Property

        Public Sub Initialize(ByVal host As Plugin.IPluginHost) _
            Implements Plugin.IPlugin.Initialize

            Me._host = host
        End Sub

        '(以下省略)
    End Class
End Namespace

 FindStringプラグインは、フォームを表示するプラグインのサンプルです。検索ダイアログを表示して、指定された文字列を検索します。FileHistoryプラグインに関しては、後ほど説明します。

ホストの作成

 ホストアプリケーションは、Windowsアプリケーションとして作成し、フォームにRichTextBoxと、MainMenuStatusBarコントロールを配置します。さらに、IPluginHostインターフェイスを実装します。サンプルでは、プロジェクト「TextEditorForm」がホストです。

C#
namespace PluginTextEditor
{
    public class TextEditorForm :
        System.Windows.Forms.Form, Plugin.IPluginHost
    {
        public RichTextBox RichTextBox
        {
            get
            {
                return mainRichTextBox;
            }
        }

        public void ShowMessage(Plugin.IPlugin plugin, string msg)
        {
            //ステータスバーに表示する
            mainStatusbar.Text = msg;
        }

        //(以下省略)
    }
}
VB.NET
Namespace MainApplication
    Public Class TextEditorForm
        Inherits System.Windows.Forms.Form
        Implements Plugin.IPluginHost

        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

        '(以下省略)
    End Class
End Namespace

 ホストの作成で問題となるのは、有効なプラグインをどのように探すか、そして、プラグインのインスタンスをどのように作成するかの2点でしょう。

 有効なプラグインを探すには、指定されたプラグインフォルダにあるDLLファイルをAssembly.LoadFromメソッドで読み込み、その中に含まれている型を列挙し、Type.GetInterfaceメソッドによりIPluginインターフェイスを実装したクラスであるか調べることにします。

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

 これらの処理は、PluginInfoクラスで行っています。

C#
using System;

namespace PluginTextEditor
{
    /// <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(string pluginDir)
        {
            System.Collections.ArrayList plugins =
                new System.Collections.ArrayList();
            //IPlugin型の名前
            string ipluginName = typeof(Plugin.IPlugin).FullName;

            if (!System.IO.Directory.Exists(pluginDir))
                throw new ApplicationException(
                    "プラグインフォルダ\"" + pluginDir +
                    "\"が見つかりませんでした。");

            //.dllファイルを探す
            string[] dlls =
                System.IO.Directory.GetFiles(pluginDir, "*.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);
                //初期化
                plugin.Initialize(host);
                return plugin;
            }
            catch
            {
                return null;
            }
        }
    }
}
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( _
            ByVal pluginDir As String) As PluginInfo()
            Dim plugins As New System.Collections.ArrayList
            'IPlugin型の名前
            Dim ipluginName As String = _
                GetType(Plugin.IPlugin).FullName

            If Not System.IO.Directory.Exists(pluginDir) Then
                Throw New ApplicationException( _
                    "プラグインフォルダ""" + pluginDir + _
                    """が見つかりませんでした。")
            End If

            '.dllファイルを探す
            Dim dlls As String() = _
                System.IO.Directory.GetFiles(pluginDir, "*.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( _
            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)
                '初期化
                plugin.Initialize(host)
                Return plugin
            Catch
                Return Nothing
            End Try
        End Function
    End Class
End Namespace

応用

 以上がプラグイン機能を実現する基本的な方法で、同様のことが下に示した参考資料の1~6のリンクでも紹介されています。ここからはその応用として、使えそうなアイデアを幾つか紹介します。

IPluginHostにイベントを追加する

 例えばエディタでファイルを開いた時や保存した時に、そのことをプラグイン側で知ることができれば、プラグインでできることが広がります。ここではその例として、IPluginHostインターフェイスにファイルを開いた直後に発生するOpenedFileイベントを追加します。

C#
namespace Plugin
{
    /// <summary>
    /// OpenedFileイベントのデリゲート
    /// </summary>
    public delegate void OpenedFileEventHandler(
        object sender, OpenedFileEventArgs e);

    public interface IPluginHost
    {
        /// <summary>
        /// ファイルを開いた直後に発生するイベント
        /// </summary>
        event OpenedFileEventHandler OpenedFile;
    }

    //(以下省略)
}
VB.NET
Namespace Plugin
    ''' <summary>
    ''' OpenedFileイベントのデリゲート
    ''' </summary>
    Public Delegate Sub OpenedFileEventHandler( _
        ByVal sender As Object, ByVal e As OpenedFileEventArgs)

    Public Interface IPluginHost
        ''' <summary>
        ''' ファイルを開いた直後に発生するイベント
        ''' </summary>
        Event OpenedFile As OpenedFileEventHandler
    End Interface

    '(以下省略)
End Namespace

 このイベントを使ったプラグインの例が、サンプルのプロジェクト「FileHistory」です。このプラグインでは、エディタで開かれたファイルのパスを保存しています。

プラグインでMenuItemオブジェクトを作成する

 プラグインをメニューに表示する場合、そのMenuItemはホストで作成するのが普通でしょう(少なくとも下に挙げた参考資料のサンプルではそうなっています)。しかし、IPluginインターフェイスにMenuItem型のプロパティを追加し、プラグイン側でMenuItemオブジェクトを作成するという方法もあります。プラグインが自分のMenuItemを管理すべきと考えれば、むしろこの方がよいでしょう。

 サンプルのFileHistoryプラグインではサブメニューを持つMenuItemを作成し、下図のようにサブメニューで選択したファイルを開けるようにしています。

サブメニューを持つプラグイン
サブメニューを持つプラグイン

プラグインの設定を行う

 プラグインによっては、独自の設定が必要なものもあるでしょう。そこで、IPluginインターフェイスにShowSetupDialogメソッドを追加し、プラグインの設定ダイアログを表示できるようにします。さらにHasSetupDialogプロパティを追加し、設定ダイアログがあるかないか取得できるようにすれば、事前に設定の有無を知ることができ、下図のように、ユーザーがプラグインを選択した時に設定があれば[設定]ボタンを有効に、なければ無効にするといったことができるようになります。

プラグインの設定ダイアログ
プラグインの設定ダイアログ

 サンプル内で設定のあるプラグインの例は、FileHistoryプラグインです。

 補足:アプリケーションの設定がTabControlで行われる時は、インターフェイスが自分の設定のためのTabPageオブジェクトを作成し、アプリケーションの設定に追加できるようするという方法もあるでしょう。

まとめ

 この記事では、.NET Frameworkによりプラグイン機能を実現させる方法を、同機能を持ったテキストエディタの作成を例に解説しました。プラグイン機能実現のポイントを簡単にまとめると次のようになります。

  • プラグインはDLLとして作成し、ホストからはリフレクションを使ってプラグインの機能を呼び出す。
  • プラグイン及びホストで必ず実装すべきインターフェイスをあらかじめ作成しておけば、あいまいさを排除することができ、有益である。

参考資料

  1. Developer Fusion 『Writing Plugin-Based Applications - Introduction』 Tim Dawson 著
  2. SADeveloper.Net 『PluginFX - An extensible plugin framework in C#』 willemf 著、2004年1月
  3. The Code Project 『Plugin Architecture using C#』 Shoki 著、2003年8月
  4. The Code Project 『Plug-ins in C#』 Redth 著、2004年5月
  5. DevSource 『Building Plug-ins with C# .NET: Part 1』 Nathan Good 著、2004年5月
  6. .NETプログラミング研究 『第39、40号』 どぼん! 著
  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2006/04/03 01:07 プラグインフォルダにPlugin.dllを入れるとエラーが出る不具合への対処を施しました。対処法に関して詳しくは、ソースに同梱されているREADMEをご覧ください。

著者プロフィール

  • どぼん!(ドボン!)

    DOBON.NET内で.NET Frameworkの機能を紹介したWebサイト.NET Tipsやメールマガジン「.NETプログラミング研究」の発行人。

All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5