■ プラグイン詳細・2 ■
~ プラグイン機能を持つテキストエディタの作成 ~ ■ はじめに
Adobe Photoshop や Becky! Internet Mail 等のアプリケーションでは「プラグイン」(又は、「アドイン」、 「エクステンション」等)と呼ばれるプログラムをインストールする事に依り、機能を拡張する事が出 来る。此の記事では此の様なプラグイン機能を持ったアプリケーションの作り方を、プラグイン対応の テキストエディタを作成する事に依り、説明する。 此処で紹介するプラグイン機能は、Becky!の様に、プラグイン本体で有る DLL ファイルを指定された フォルダにコピーする事に依り、プラグインを使用するアプリケーション(ホスト)が自動的にプラグ インを認識すると謂う物で有る。 プラグイン機能の有るテキストエディタ 猶、プラグイン機能の解説が目的の為、テキストエディタはフォームにRichTextBox を貼り付けた丈の 貧弱な物ですので、テキストエディタ作成の参考にはなりません。 ■ 必要な環境
サンプルは 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.vb」 (VB.NET)、「IPlugin.cs」 (C#) Visual Basic 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 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 インターフェイスを実装する事に依り、プラグインを作成する。先程と同様にクラスラ イブラリのプロジェクトを新規作成し、「Plugin.dll」を参照に加え、IPlugin を実装したクラスを作成 する。サンプルには3 つのプラグインのプロジェクト(「CountChar」、「FindString」、「FileHistory」) が含まれて居る。 此のうち最も単純な、文章の文字数を数える丈のプラグインで有る CountChar プラグインのコードの 一部を以下に示す。 「CountChars.vb」 (VB.NET)、「CountChars.cs」 (C#) Visual Basic 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 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; } //(以下省略) } } FindString プラグインは、フォームを表示するプラグインのサンプルで有る。検索ダイアログを表示し て、指定された文字列を検索する。FileHistory プラグインに関しては、後ほど説明する。 ■ ホストの作成 ホストアプリケーションは、Windows アプリケーションと仕て作成し、フォームに RichTextBox と、 MainMenu、StatusBar コントロールを配置する。更に、IPluginHost インターフェイスを実装する。 サンプルでは、プロジェクト「TextEditorForm」がホストで有る。 Visual Basic 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 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; } //(以下省略) } } ホストの作成で問題と成るのは、有効なプラグインを何の様に探すか、然して、プラグインのインスタ ンスを何の様に作成するかの2 点でしょう。 有 効 な プ ラ グ イ ン を 探 す に は 、 指 定 さ れ た プ ラ グ イ ン フ ォ ル ダ に 有 る DLL フ ァ イ ル を Assembly.LoadFrom メソッドで読み込み、其の中に含まれて居る型を列挙し、Type.GetInterface メソ ッドに依りIPlugin インターフェイスを実装したクラスで有るか調べる事にする。 亦 プ ラ グ イ ン の イ ン ス タ ン ス を 作 成 す る に は 、Activator.CreateInstance メ ソ ッ ド や 、 Assembly.CreateInstance メソッド等を使用すれば良いでしょう。 此等の処理は、PluginInfo クラスで行って居る。 Visual Basic 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 C# using System; namespace PluginTextEditor { // <summary> // プラグインに関する情報 // </summary>
{
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; } } } } ■ 応用
以上がプラグイン機能を実現する基本的な方法で、同様の事が下に示した参考資料の 1~6 のリンクで も紹介されて居る。此処からは其の応用と仕て、使えそうなアイデアを幾つか紹介する。 IPluginHost にイベントを追加する 例えばエディタでファイルを開いた時や保存した時に、其の事をプラグイン側で知る事ができれば、プ ラグインで出来る事が広がります。此処では其の例と仕て、IPluginHost インターフェイスにファイル を開いた直後に発生するOpenedFile イベントを追加する。 Visual Basic 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 C# namespace Plugin { // <summary> // OpenedFile イベントのデリゲート // </summary>
public delegate void OpenedFileEventHandler( object sender, OpenedFileEventArgs e); public interface IPluginHost
{
// <summary>
// ファイルを開いた直後に発生するイベント // </summary>
event OpenedFileEventHandler OpenedFile; } //(以下省略) } 此のイベントを使ったプラグインの例が、サンプルのプロジェクト「FileHistory」で有る。此のプラグ インでは、エディタで開かれたファイルのパスを保存して居る。 プラグインでMenuItem オブジェクトを作成する プラグインをメニューに表示する場合、其のMenuItem はホストで作成するのが普通でしょう(少なく
とも下に挙げた参考資料のサンプルではそう成って居る)。併し、IPlugin インターフェイスに MenuItem 型のプロパティを追加し、プラグイン側で MenuItem オブジェクトを作成すると謂う方法 も有る。プラグインが自分のMenuItem を管理すべきと考えれば、むしろ此の方が良いでしょう。 サンプルのFileHistory プラグインではサブメニューを持つ MenuItem を作成し、下図の様にサブメニ ューで選択したファイルを開ける様にして居る。 サブメニューを持つプラグイン プラグインの設定を行う プラグインによっては、独自の設定が必要な物も有るでしょう。其処で、IPlugin インターフェイスに ShowSetupDialog メソッドを追加し、プラグインの設定ダイアログを表示出来る様にする。更に HasSetupDialog プロパティを追加し、設定ダイアログが有るかないか取得出来る様にすれば、事前に 設定の有無を知る事ができ、下図の様に、ユーザーがプラグインを選択した時に設定があれば[設定] ボタンを有効に、なければ無効にするといった事が出来る様に成る。 プラグインの設定ダイアログ サンプル内で設定の有るプラグインの例は、FileHistory プラグインで有る。
補足:アプリケーションの設定がTabControl で行われる時は、インターフェイスが自分の設定の為の TabPage オブジェクトを作成し、アプリケーションの設定に追加出来る様すると謂う方法も有るでしょ う。 ■ まとめ 此の記事では、.NET Framework に依りプラグイン機能を実現させる方法を、同機能を持ったテキスト エディタの作成を例に解説しました。プラグイン機能実現のポイントを簡単にまとめると下記の様に成 る。 ・プラグインは DLL と仕て作成し、ホストからはリフレクションを使ってプラグインの機能を呼び出 す。 ・プラグイン及びホストで必ず実装すべきインターフェイスを予め作成しておけば、あいまいさを排除 する事ができ、有益で有る。