はじめに
Adobe PhotoshopやBecky! 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」というファイル名で出力しています。以下にそのコードの一部を抜粋します。
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); //(以下省略) } }
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
プラグインのコードの一部を以下に示します。
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; } //(以下省略) } }
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
と、MainMenu
、StatusBar
コントロールを配置します。さらに、IPluginHost
インターフェイスを実装します。サンプルでは、プロジェクト「TextEditorForm」がホストです。
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; } //(以下省略) } }
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
クラスで行っています。
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; } } } }
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
イベントを追加します。
namespace Plugin { /// <summary> /// OpenedFileイベントのデリゲート /// </summary> public delegate void OpenedFileEventHandler( object sender, OpenedFileEventArgs e); public interface IPluginHost { /// <summary> /// ファイルを開いた直後に発生するイベント /// </summary> event OpenedFileEventHandler OpenedFile; } //(以下省略) }
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として作成し、ホストからはリフレクションを使ってプラグインの機能を呼び出す。
- プラグイン及びホストで必ず実装すべきインターフェイスをあらかじめ作成しておけば、あいまいさを排除することができ、有益である。
参考資料
- Developer Fusion 『Writing Plugin-Based Applications - Introduction』 Tim Dawson 著
- SADeveloper.Net 『PluginFX - An extensible plugin framework in C#』 willemf 著、2004年1月
- The Code Project 『Plugin Architecture using C#』 Shoki 著、2003年8月
- The Code Project 『Plug-ins in C#』 Redth 著、2004年5月
- DevSource 『Building Plug-ins with C# .NET: Part 1』 Nathan Good 著、2004年5月
- .NETプログラミング研究 『第39、40号』 どぼん! 著