補足:CodeZineの「.NETによるプラグイン機能を持つテキストエディタの作成」でも詳しく説明しています。
Adobe PhotoshopやBecky! Internet Mailなどのアプリケーションでは、「プラグイン」(または、「アドイン」、「エクステンション」等)と呼ばれるプログラムをインストールすることにより、機能を追加することができるようになっています。ここでは、このようなプラグイン機能を持ったアプリケーションの作り方を考えます。(プラグインが何だか分からないという方は、「アスキー デジタル用語辞典」や「IT用語辞典 e-Words」等をご覧ください。)
早速ですが、プラグイン機能の実現のために参考になりそうな記事を以下にいくつか紹介します。
これらの記事で紹介されている方法は基本的にはほとんど同じで、それは、インターフェイスを使用するという方法です。つまり、プラグインとして必要となる機能をプロパティやメソッド(さらに、イベントやインデクサ)として持つインターフェイスをあらかじめ用意しておき、このインターフェイスを実装したクラスとしてプラグインを作成するのです。
理屈は置いておき、実際にプラグインを作成してみましょう。
ここで作成するプラグインは、プラグインの名前を返す機能と、渡された文字列を処理して、結果を文字列として返す機能を有するものとし、それぞれの機能のためにNameプロパティとRunメソッドを持つインターフェイスを作ります。
Visual Studioでは、クラスライブラリのプロジェクトを作成し、次のようなコードを書き、ビルドします。.NET SDKの場合は、/target:libraryコンパイラオプションにより、コードライブラリを作成します。なお、ここで作成されたアセンブリファイル名は、"Plugin.dll"であるとします。
注意:Visual Studioの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
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"という名前のアセンブリファイルとしてビルドするものとします。
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
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")を持ちます。このクラスのコードを以下に示します。
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
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クラスを使ったメインのクラス(エントリポイント)のサンプルは、次のようになります。
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
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アプリケーションでプラグイン機能を実現する方法を考えます。ここでは具体的に、プラグインの使用できる簡単なエディタを作成します。
あくまでプラグイン機能を説明することが目的ですので、エディタは思い切り単純にし、フォームに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インターフェイスがプラグインのホストのためのインターフェイスです。
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
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クラスのコードは次のようになります。
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
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を検索できるようにします。
'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
//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)を作成します。コードは、次のようになります。
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
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クラスは次のようなコードです。
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
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フォームデザイナが作成したコードを除く)を示します。
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
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"をコピーしてください。うまくいくと、「プラグイン」メニューに「文字数取得」と「文字列の検索」が追加され、プラグインの機能を呼び出すことができるようになります。
以上でプラグイン機能を実現させる方法に関する解説はおしまいです。ここで紹介した知識を応用することにより、より複雑なプラグインも作成できるでしょう。この記事を読んで、プラグインを使ったアプリケーションを作ってみようと思われる方が一人でもいらっしゃるならばうれしいのですが。
(この記事は、「.NETプログラミング研究 第39,40号」で紹介したものです。)
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。