C#でプラグイン機能を作る!!!
それだけだっ!!
ただ、それだけだと他のブログさんの記事と似たような内容になるので、僕なりにちょっとだけ工夫しています
CUI版とGUI版の2種類作ってます
CUI版は他の記事と同じような内容ですが、GUI版は実践的に使うならこういうことできると幸せになれるかな、と思ったことを実装しています
参考になれば嬉しいです
ぜひこちらもご覧ください
私にとっては、C#のプラグインの記事の中では、この記事が一番シンプルで分かりやすかったです。
その他の記事もとても参考になりました。
ありがとうございました。
この記事を読む前に、ぜひ上記の記事を先にご覧ください。
Contents
まずはC#でプラグインの概要
この記事を読まれる方ならプラグインはお分かりでしょうから、ソースコードの構成とか大雑把に説明します
以下の三つがあればプラグイン機能を試すことができます
- メインのプログラム
- ファイル形式:EXE
- プラグインのDLLが所定のフォルダにあれば、プラグインを読み込んでプラグインを実行します
プラグインのDLLが所定のフォルダに無ければ何事もなく何もしません - この記事ではGUI版とCUI版の二種類を解説します
CUIで解説されても、実践ではGUIでしょうから応用しづらいかと思い、GUI版も解説します
と言っても、どっちも作り方は基本的には同じです
- プラグインのベース
- ファイル形式:DLL
- プラグイン作成者に対してこれを提供します
- プラグインの作成にあたってこのプラグインベースを参照設定してもらいます
抽象メソッドとか定義しておけば、プラグイン作成者がどういうメソッドを書けばいいか分かりやすくなります - 無くてもプラグインは作れますが、色々と面倒なのでベースがある方がいいでしょう
- ベース無しで作るなら以下の記事が参考になるでしょう
- プラグイン
- ファイル形式:DLL
- プラグインベースを取り込んで継承して作ります
ソースコードを見た方が早い!
ソースコード貼り付けておきます
コメント書いてるので、それを読んでください
解説はしません
書くのが面倒ってのもありますが(笑)、ソースコードとコメントだけ見れば充分理解できると思います
プロジェクト一式はGitHubに置いてます
ダウンロードしてもらって、Visual Studio 2017とかで参照設定し直してビルドすれば使えるでしょう
GitHubはこちら >> PluginSample_AbstractClassVersion | GitHub
MainProgramCUIプロジェクト
using System; namespace MainProgramCUI { class Program { static void Main(string[] args) { // プラグインをロードして実行 var plugin = new PluginAccessor(); var numDll = plugin.LoadPlugins(); Console.WriteLine($"[Main] numDll={numDll}"); Console.WriteLine(""); Console.WriteLine("[Main] 全てのプラグイン実行========="); plugin.AllPluginExec(); Console.WriteLine(""); Console.WriteLine("[Main] 1つ目のプラグイン実行========="); var idxDll = 0; plugin.PluginShow(idxDll); plugin.PluginSetNo(idxDll, 5); var getno = plugin.PluginGetNo(idxDll); Console.WriteLine($"[Main] getno1={getno}"); Console.WriteLine(""); Console.WriteLine("[Main] 2つ目のプラグイン実行========="); idxDll = 1; plugin.PluginShow(idxDll); plugin.PluginSetNo(idxDll, 7); getno = plugin.PluginGetNo(idxDll); Console.WriteLine($"[Main] getno2={getno}"); Console.WriteLine(""); // 処理結果を見るために、コンソール画面が落ちないように一時停止 Console.ReadLine(); } } }
using PluginBaseDLL; using System; using System.Collections.Generic; using System.IO; using System.Reflection; // 参照の追加:PluginBaseDLL.dll // このEXEと同じ階層のPluginフォルダにPluginSample1.dllとPluginSample2.dllを入れておく namespace MainProgramCUI { public class PluginAccessor { // 今回のサンプルではこのリストにPluginSample1.dllとPluginSample2.dllが登録される List_listPluginClass = new List (); /// /// プラグインフォルダに入ってるプラグイン(DLL)を読み取る /// ///ロードできたDLLの個数 public int LoadPlugins() { //--------------------------------------------------------------------------------- // プラグインDLLの格納されたフォルダをEXEと同じ階層にあるPluginsフォルダとする //--------------------------------------------------------------------------------- string strPluginFolder = Environment.CurrentDirectory + "\\Plugins"; //--------------------------------------------------------------------------------- // プラグインフォルダ内のDLLファイルを取得 //--------------------------------------------------------------------------------- // 今回のサンプルではPluginSample1.dllとPluginSample2.dllと取得できるはず string[] aryDllFilePath = Directory.GetFiles(strPluginFolder, "*.dll"); //--------------------------------------------------------------------------------- // PluginBaseClassの派生クラスのインスタンスを作成し、aryPluginBaseに追加 //--------------------------------------------------------------------------------- foreach (var sDLLFilePath in aryDllFilePath) { //--------------------------------------------------------------------------------- // アセンブリをロード //--------------------------------------------------------------------------------- var assembly = Assembly.LoadFile(sDLLFilePath); if (assembly == null) { continue; } //--------------------------------------------------------------------------------- // アセンブリ内の型定義を取得 //--------------------------------------------------------------------------------- Type[] types = assembly.GetTypes(); //--------------------------------------------------------------------------------- // PluginInterfaceまたはPluginBaseClassを継承するクラスをデフォルト // コンストラクタ経由でインスタンスを作成→各配列に追加する //--------------------------------------------------------------------------------- foreach (var type in types) { //----------------------------------------------------------------------------- // クラス以外の型、抽象クラス、非公開クラス、外部利用不可なクラスは除く //----------------------------------------------------------------------------- if (!type.IsClass || type.IsAbstract || type.IsNotPublic || !type.IsVisible) { continue; } //----------------------------------------------------------------------------- // ベースクラスPluginBaseClassを継承していることを確認 //----------------------------------------------------------------------------- if (type.IsSubclassOf(typeof(PluginBaseClass))) { // (公開)デフォルトコンストラクタを取得 var ci = type.GetConstructor(Type.EmptyTypes); if (ci == null) { // デフォルトコンストラクタが無いだとっ!? continue; } // インスタンス作成 // カンケー無いけど、Invokeってのは呼び出すっていう意味 var instance = ci.Invoke(new object[] { }); if (instance == null) { // インスタンスを作成出来無いだとっ!? continue; } // チェックを無事通ったものだけがリストに登録される _listPluginClass.Add((PluginBaseClass)instance); } } } //----------------------------------------------------------------------------- // CUIで動くことを指定 //----------------------------------------------------------------------------- foreach (var plugin in _listPluginClass) { plugin.action_mode = PluginBaseClass.ACTION_MODE.CUI; } //----------------------------------------------------------------------------- // ロードできたDLLの個数を返す //----------------------------------------------------------------------------- return _listPluginClass.Count; } ////// 全てのプラグインを実行する /// ※先にLoadPlugins()でプラグインを読み込んでいること /// public void AllPluginExec() { foreach (var plugin in _listPluginClass) { plugin.Show(); plugin.SetNo(3); var ret = plugin.GetNo(); Console.WriteLine($"[PluginAccessor] ret={ret}"); Console.WriteLine("-------------"); } } ////// 個別にプラグインのShow()メソッドを実行 /// ※先にLoadPlugins()でプラグインを読み込んでいること /// ※本来なら引数のidxDllのチェックが必要だが省略 /// /// public void PluginShow(int idxDll) { _listPluginClass[idxDll].Show(); } ////// 個別にプラグインのSetNo()メソッドを実行 /// ※先にLoadPlugins()でプラグインを読み込んでいること /// ※本来なら引数のidxDllのチェックが必要だが省略 /// /// /// public void PluginSetNo(int idxDll, int no) { _listPluginClass[idxDll].SetNo(no); } ////// 個別にプラグインのGetNo()メソッドを実行 /// ※先にLoadPlugins()でプラグインを読み込んでいること /// ※本来なら引数のidxDllのチェックが必要だが省略 /// /// ///public int PluginGetNo(int idxDll) { return _listPluginClass[idxDll].GetNo(); } } }
MainProgramGUI
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace MainProgramGUI { static class Program { ////// アプリケーションのメイン エントリ ポイントです。 /// [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using static PluginBaseDLL.PluginBaseClass; namespace MainProgramGUI { public partial class MainForm : Form { ////////// ログレベル ///// //public enum LOG_LEVEL //{ // TRACE, // DEBUG, // INFO, // WARN, // ERROR, // FATAL, //} ////// フォームのコンストラクタ /// public MainForm() { InitializeComponent(); } ////// 画面部品のリストボックスにログ出力 /// /// /// private void LogOut(LOG_LEVEL level, string log) { listLog.Items.Add($"{DateTime.Now} [{level.ToString()}] {log}"); listLog.SelectedIndex = listLog.Items.Count - 1; } #region イベントハンドラ ////// Thread Startボタンのクリックイベント /// /// /// private void btnThreadStart_Click(object sender, EventArgs e) { cancelTokenSource = new CancellationTokenSource(); cancelToken = cancelTokenSource.Token; var task = new Task(() => ThreadWorkerAsync()); task.Start(); // ↑じゃなくて、↓のawaitならスレッドがメインのままなのでInvoke()なんか使わなくてもいいはず //await ThreadWorkerAsync(); // ↑await使うならbtnThreadStart_Click()をasyncにする listLog.Items.Add("-------------"); } ////// Thread Stopボタンのクリックイベント /// /// /// private void btnThreadStop_Click(object sender, EventArgs e) { cancelTokenSource.Cancel(); } #endregion #region UIスレッドとワーカースレッドのI/F ////// ワーカースレッドからUIスレッドの画面部品のリストボックスにログを出力するためのデリゲート /// /// /// delegate void LogOutDelegate(LOG_LEVEL level, string log); ////// ワーカースレッドからUIスレッドの画面部品のリストボックスにログを出力 /// /// /// public void LogOutInvoke(LOG_LEVEL level, string log) { // カンケー無いけど、Invokeってのは呼び出すっていう意味 Invoke(new LogOutDelegate(LogOut), level, log); } #endregion #region ワーカースレッド // ワーカースレッドのキャンセル CancellationTokenSource cancelTokenSource; CancellationToken cancelToken; ////// ワーカースレッド本体 /// ///private async Task ThreadWorkerAsync() { // プラグインをロードして実行 var plugin = new PluginAccessor(); var numDll = plugin.LoadPlugins(); LogOutInvoke(LOG_LEVEL.INFO, $"[Main] numDll={numDll}"); LogOutInvoke(LOG_LEVEL.INFO, ""); while (!cancelToken.IsCancellationRequested) { LogOutInvoke(LOG_LEVEL.INFO, "[Main] 全てのプラグイン実行========="); plugin.AllPluginExec(new PluginLogWriter(this)); LogOutInvoke(LOG_LEVEL.INFO, ""); LogOutInvoke(LOG_LEVEL.INFO, "[Main] 1つ目のプラグイン実行========="); var idxDll = 0; plugin.PluginShow(idxDll); plugin.PluginSetNo(idxDll, 5); var getno = plugin.PluginGetNo(idxDll); LogOutInvoke(LOG_LEVEL.INFO, $"[Main] getno1={getno}"); LogOutInvoke(LOG_LEVEL.INFO, ""); LogOutInvoke(LOG_LEVEL.INFO, "[Main] 2つ目のプラグイン実行========="); idxDll = 1; plugin.PluginShow(idxDll); plugin.PluginSetNo(idxDll, 7); getno = plugin.PluginGetNo(idxDll); LogOutInvoke(LOG_LEVEL.INFO, $"[Main] getno2={getno}"); LogOutInvoke(LOG_LEVEL.INFO, ""); // ワーカースレッドから画面部品にはアクセスできない // 画面部品にアクセスできるのはUIスレッドのみ // なので、↓の二行は例外になる // listLog.Items.Add($"{DateTime.Now} [{LOG_LEVEL.ERROR}] ワーカースレッド"); // LogOut(LOG_LEVEL.INFO, "ワーカースレッド"); // ワーカースレッドから画面部品にアクセスするにはInvoke()を使う // ↓ LogOutInvoke(LOG_LEVEL.INFO, "ワーカースレッド実行中"); LogOutInvoke(LOG_LEVEL.INFO, ""); LogOutInvoke(LOG_LEVEL.INFO, ""); await Task.Delay(3000); } LogOutInvoke(LOG_LEVEL.INFO, "ワーカースレッド終了"); } #endregion } }
using PluginBaseDLL; using System; using System.Collections.Generic; using System.IO; using System.Reflection; // 参照の追加:PluginBaseDLL.dll // このEXEと同じ階層のPluginフォルダにPluginSample1.dllとPluginSample2.dllを入れておく namespace MainProgramGUI { public class PluginAccessor { // 今回のサンプルではこのリストにPluginSample1.dllとPluginSample2.dllが登録される List_listPluginClass = new List (); /// /// プラグインフォルダに入ってるプラグイン(DLL)を読み取る /// ///ロードできたDLLの個数 public int LoadPlugins() { //--------------------------------------------------------------------------------- // プラグインDLLの格納されたフォルダをEXEと同じ階層にあるPluginsフォルダとする //--------------------------------------------------------------------------------- string strPluginFolder = Environment.CurrentDirectory + "\\Plugins"; //--------------------------------------------------------------------------------- // プラグインフォルダ内のDLLファイルを取得 //--------------------------------------------------------------------------------- // 今回のサンプルではPluginSample1.dllとPluginSample2.dllと取得できるはず string[] aryDllFilePath = Directory.GetFiles(strPluginFolder, "*.dll"); //--------------------------------------------------------------------------------- // PluginBaseClassの派生クラスのインスタンスを作成し、aryPluginBaseに追加 //--------------------------------------------------------------------------------- foreach (var sDLLFilePath in aryDllFilePath) { //--------------------------------------------------------------------------------- // アセンブリをロード //--------------------------------------------------------------------------------- var assembly = Assembly.LoadFile(sDLLFilePath); if (assembly == null) { continue; } //--------------------------------------------------------------------------------- // アセンブリ内の型定義を取得 //--------------------------------------------------------------------------------- Type[] types = assembly.GetTypes(); //--------------------------------------------------------------------------------- // PluginInterfaceまたはPluginBaseClassを継承するクラスをデフォルト // コンストラクタ経由でインスタンスを作成→各配列に追加する //--------------------------------------------------------------------------------- foreach (var type in types) { //----------------------------------------------------------------------------- // クラス以外の型、抽象クラス、非公開クラス、外部利用不可なクラスは除く //----------------------------------------------------------------------------- if (!type.IsClass || type.IsAbstract || type.IsNotPublic || !type.IsVisible) { continue; } //----------------------------------------------------------------------------- // ベースクラスPluginBaseClassを継承していることを確認 //----------------------------------------------------------------------------- if (type.IsSubclassOf(typeof(PluginBaseClass))) { // (公開)デフォルトコンストラクタを取得 var ci = type.GetConstructor(Type.EmptyTypes); if (ci == null) { // デフォルトコンストラクタが無いだとっ!? continue; } // インスタンス作成 // カンケー無いけど、Invokeってのは呼び出すっていう意味 var instance = ci.Invoke(new object[] { }); if (instance == null) { // インスタンスを作成出来無いだとっ!? continue; } // チェックを無事通ったものだけがリストに登録される _listPluginClass.Add((PluginBaseClass)instance); } } } //----------------------------------------------------------------------------- // GUIで動くことを指定 //----------------------------------------------------------------------------- foreach (var plugin in _listPluginClass) { plugin.action_mode = PluginBaseClass.ACTION_MODE.GUI; } //----------------------------------------------------------------------------- // ロードできたDLLの個数を返す //----------------------------------------------------------------------------- return _listPluginClass.Count; } ////// 全てのプラグインを実行する /// ※先にLoadPlugins()でプラグインを読み込んでいること /// public void AllPluginExec(TextWriter newOut) { foreach (var plugin in _listPluginClass) { plugin.SetOut(newOut); plugin.Show(); plugin.SetNo(3); var ret = plugin.GetNo(); Console.WriteLine($"[PluginAccessor] ret={ret}"); Console.WriteLine("-------------"); } } ////// 個別にプラグインのShow()メソッドを実行 /// ※先にLoadPlugins()でプラグインを読み込んでいること /// ※本来なら引数のidxDllのチェックが必要だが省略 /// /// public void PluginShow(int idxDll) { _listPluginClass[idxDll].Show(); } ////// 個別にプラグインのSetNo()メソッドを実行 /// ※先にLoadPlugins()でプラグインを読み込んでいること /// ※本来なら引数のidxDllのチェックが必要だが省略 /// /// /// public void PluginSetNo(int idxDll, int no) { _listPluginClass[idxDll].SetNo(no); } ////// 個別にプラグインのGetNo()メソッドを実行 /// ※先にLoadPlugins()でプラグインを読み込んでいること /// ※本来なら引数のidxDllのチェックが必要だが省略 /// /// ///public int PluginGetNo(int idxDll) { return _listPluginClass[idxDll].GetNo(); } } }
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using static MainProgramGUI.MainForm; using static PluginBaseDLL.PluginBaseClass; namespace MainProgramGUI { public class PluginLogWriter : TextWriter { public override Encoding Encoding => Encoding.UTF8; MainForm _frm = null; ////// コンストラクタ /// /// 呼び元のForm public PluginLogWriter(MainForm frm) { this._frm = frm; } ////// /// /// public override void WriteLine(string value) { _frm.LogOutInvoke(LOG_LEVEL.ERROR, value); } public override void WriteLine(string format, object arg0) { LOG_LEVEL level = (LOG_LEVEL)arg0; _frm.LogOutInvoke(level, format); } } }
PluginBaseDLL
using System; using System.IO; namespace PluginBaseDLL { ////// プラグインベースのクラス /// public abstract class PluginBaseClass { ////// 動作モード /// GUIで動くのか、CUIで動くのか /// public enum ACTION_MODE { CUI, GUI, } public ACTION_MODE action_mode { get; set; } = ACTION_MODE.CUI; ////// ログレベル /// MainProgramGUIのLOG_LEVELと同じもの /// 本当は一元管理にして、MainProgramGUIとPluginBaseで共通的に参照するのがいいのだろうけど /// そこまでするのは面倒なのでMainProgramGUIからコピるだけにしておく /// public enum LOG_LEVEL { TRACE, DEBUG, INFO, WARN, ERROR, FATAL, } ////// 表示メソッド /// public abstract void Show(); ////// 数値をセット /// /// public abstract void SetNo(int no); ////// 数値をゲット /// ///public abstract int GetNo(); /// /// 標準出力先を変更 /// /// 呼び側で自作のTextWriter系クラスを使ってログ出力する /// 今回のサンプルでは、画面部品のログ用リストボックスに出力することになる /// 子クラスでこれを触る必要は無い /// オーバーライド禁止 /// /// 子クラスでConsole.WriteLine(LOG_LEVEL.INFO, "ログの文言だよ~ん");って感じで書いてもらえば /// 呼び側で自作のTextWriter系クラスへ渡すことができる /// /// public void SetOut(TextWriter newOut) { Console.SetOut(newOut); } } }
PluginSample1
using PluginBaseDLL; using System; // 参照の追加:PluginBaseDLL.dll // GUIモードの時のLOG_LEVELの設定値は適当なので意味はない、呼び出し側でそのレベルが取れていることを確認したいだけ namespace PluginSample1 { public class PluginSampleClass1 : PluginBaseClass { private int _no; ////// 表示メソッド /// public override void Show() { if (action_mode == ACTION_MODE.GUI) { // GUIモード Console.WriteLine("プラグインサンプル1のShowメソッドG", LOG_LEVEL.WARN); } else { // GUIモード以外(CUIモードと、無いと思うけど予期しないモード) Console.WriteLine("プラグインサンプル1のShowメソッドC"); } } ////// 数値をセット /// /// public override void SetNo(int no) { //--------------------------------------------------------------------------------- // 単にログを出したいだけ //--------------------------------------------------------------------------------- if (action_mode == ACTION_MODE.GUI) { // GUIモード Console.WriteLine($"プラグインサンプル1のSetNoメソッドG:no={no}", LOG_LEVEL.FATAL); } else { // GUIモード以外(CUIモードと、無いと思うけど予期しないモード) Console.WriteLine($"プラグインサンプル1のSetNoメソッドC:no={no}"); } //--------------------------------------------------------------------------------- // このメソッドでやりたいこと //--------------------------------------------------------------------------------- _no = no; } ////// 数値をゲット /// ///public override int GetNo() { //--------------------------------------------------------------------------------- // 単にログを出したいだけ //--------------------------------------------------------------------------------- if (action_mode == ACTION_MODE.GUI) { // GUIモード Console.WriteLine($"プラグインサンプル1のGetNoメソッドG:no={_no}", LOG_LEVEL.ERROR); } else { // GUIモード以外(CUIモードと、無いと思うけど予期しないモード) Console.WriteLine($"プラグインサンプル1のGetNoメソッドC:no={_no}"); } //--------------------------------------------------------------------------------- // このメソッドでやりたいこと //--------------------------------------------------------------------------------- return _no + 10; } } }
PluginSample2
using PluginBaseDLL; using System; // 参照の追加:PluginBaseDLL.dll // GUIモードの時のLOG_LEVELの設定値は適当なので意味はない、呼び出し側でそのレベルが取れていることを確認したいだけ namespace PluginSample2 { public class PluginSampleClass2 : PluginBaseClass { private int _no; ////// 表示メソッド /// public override void Show() { if (action_mode == ACTION_MODE.GUI) { // GUIモード Console.WriteLine("プラグインサンプル2のShowメソッドG", LOG_LEVEL.TRACE); } else { // GUIモード以外(CUIモードと、無いと思うけど予期しないモード) Console.WriteLine("プラグインサンプル2のShowメソッドC"); } } ////// 数値をセット /// /// public override void SetNo(int no) { //--------------------------------------------------------------------------------- // 単にログを出したいだけ //--------------------------------------------------------------------------------- if (action_mode == ACTION_MODE.GUI) { // GUIモード Console.WriteLine($"プラグインサンプル2のSetNoメソッドG:no={no}", LOG_LEVEL.WARN); } else { // GUIモード以外(CUIモードと、無いと思うけど予期しないモード) Console.WriteLine($"プラグインサンプル2のSetNoメソッドC:no={no}"); } //--------------------------------------------------------------------------------- // このメソッドでやりたいこと //--------------------------------------------------------------------------------- _no = no; } ////// 数値をゲット /// ///public override int GetNo() { //--------------------------------------------------------------------------------- // 単にログを出したいだけ //--------------------------------------------------------------------------------- if (action_mode == ACTION_MODE.GUI) { // GUIモード Console.WriteLine($"プラグインサンプル2のGetNoメソッドG:_no={_no}", LOG_LEVEL.DEBUG); } else { // GUIモード以外(CUIモードと、無いと思うけど予期しないモード) Console.WriteLine($"プラグインサンプル2のGetNoメソッドC:_no={_no}"); } //--------------------------------------------------------------------------------- // このメソッドでやりたいこと //--------------------------------------------------------------------------------- return _no + 20; } } }
さいごに、
でもまぁ、プラグイン機能があるとなんかカッコイイよね!(笑)
他の言語でのプラグイン機能の作り方ってよく知らないんだけど、C#の場合は結構簡単ですね
DLLを動的に読み込んでリフレクション使ってるだけだし、メソッドの呼び方も普通だし、「挑戦」というほど難しくないけど挑戦してみてはどうでしょうか
参考書籍
プログラミング のレッスンに興味がある方、レッスン内容を聞いてみたい方、なんなりとお問い合わせください。
無料体験レッスンもありますのでお気軽にどうぞ!!!