メニューとツールバー

GOTO MFCを使ったPluginオブジェクトの設計と実装 TOP

一般のMFCアプリケーションを思い浮かべてください。
このアプリケーションは複数の種類の異なったドキュメントファイルを扱うことができるとします。
たとえば、テキストファイル、HTMLファイルのいずれも編集できるエディターで考えて見ます。
このエディターはドキュメントが開かれていないとき、あるいはテキスト、HTMLのいずれが開かれている場合でも常に、[ファイル]-[新規作成] メニューは選択することができます。
しかし、[書式]-[リンクの設定] メニューはHTMLファイルを編集しているときにしか表示されません(そうあるべきです)。

さて、このアプリケーションをプログラマの視点から考えてみましょう。
まず、全ドキュメント共通のメニューリソースを1つだけ用意します。
このメニューは [ファイル] - [新規作成] や [ヘルプ] - [バージョン情報] などのメニューアイテムを含んでいます。
そして、アプリケーションでサポートされている1つ1つのドキュメントごとに固有なメニューを別に用意します。
テキストドキュメントであれば [編集] - [大文字小文字変換] などのメニューが考えられます。
また、HTMLドキュメントであれば、[編集] - [タグの挿入] 、 [書式] - [フォントサイズ]などのメニューが考えられます。

アプリケーションを起動した直後、すなわちドキュメントが何も開かれていないときには共通のメニューをそのまま表示し、ドキュメントが開かれている際には、共通のメニューにドキュメントごとの差分メニューをマージして1つのメニューとして表示できるような仕組みが作れないでしょうか?

ドキュメントの数がふえ、このアプリケーションでXMLファイル、画像ファイル、グラフファイルなどが編集できるようになったとします。

急遽 [ツール] - [環境設定] メニューが追加されることになったとします。

通常の作り方で作成したMFCアプリケーションの場合、少なくとも編集可能なドキュメントの数以上のメニューリソースをすべて修正する必要があります。面倒なだけではなく、追加し忘れ、位置の間違いなどのつまらないミスが起きるかもしれません。

しかし、ドキュメントごとにメニューをマージして使用するやり方を取れば少なくともメニューリソースの変更は1箇所だけ、すなわち共通メニュー内に [ツール] - [環境設定] を追加するだけです。

さて、メニューをプラグイン形式で追加可能な仕組みを作成する(もちろん、メニューガ選択されたときに実行される機能もプラグインとして実装します)、メニューだけではなく、編集可能なドキュメントもプラグイン可能な仕組みを作ることはできないでしょうか?

このトピックの先頭に次のようなことを書きました。

Doc/View 構造ってMFCを覚えたてのころは何のことかさっぱりわからなかった。
けど、使い慣れると(使い方を間違えなければ)それなりにありがたい代物だと思えるようになってきた。
これってプラグイン化できればすごくない?目的はいろいろあると思う。
たとえば、いくつかの種類のドキュメントを編集できるアプリケーションがあるとする。
特定の種類のドキュメントだけを編集するものを作りたい。
特定の顧客向け(だけ)にドキュメントの種類を追加したい。
カスタマイズバージョンのドキュメントを特定のユーザにだけ配布したい。
既存のドキュメントを組み合わせて新しい製品を作りたい。
需要はあると思うんだよね。誰もやらないだけで。
じゃあ、僕がやってみよう。
頭の中ではかんせいしてるんだけど、MFCを最近触ってないこともあって、更新とまってます。
でも、WTLをつかってほとんど同じことをやろうとしてるんだけどね。
COMを使えば多分実現可能です。でももっとシンプルに行きたいじゃん。

これからが本質です。最初に僕がやりたかったことです。
ちなみに、今ふと思ったのですが、GUIDに束縛されたCOMを、後から追加可能なプラグインとして使用するのは適切とはいいがたいのかも。

さて、もちろんMFCは本来そういった機能はサポートしていません。
実際それをやるにはいくつかやらなければいけないことがあります。

まずは、上でも少し触れましたが、基本となるメニューにドキュメントごとに固有なメニューをマージするための仕組みが必要です。

2つのメニューをマージするだけならさほど難しくありません。

まずはそれを実装したソースコードを紹介します。
Download - 2つのメニューをマージする
Download - サンプルプロジェクト

もちろん、単純にメニューをマージしただけでは機能がプラグイン可能であるとはいえません。
仮に、メニューリソースを保持したDLLを動的呼び出しにより、メインウィンドウのメニューにマージする仕組みを作ったとしてもです。

まず、メニューが選択されたときの処理もプラグインに実装する必要があります。
しかし、メニューが選択された際に WM_COMMANDメッセージをメインフレームからプラグインに通知するためのインターフェースがありません。

さらに、たくさんのプラグインをインストールした環境で、たとえばツールメニューからドキュメント形式変換メニューを選択したとします。
このとき、そのメニューがどのプラグインで提供されたものなのかを知るための手段も用意されていません。

また、リソースIDの問題もあります。
たとえば、あるプラグインがメニューID751に [ 範囲選択 ] というメニューを割り当てたとします。
別のプラグインが、メニューID751を[ ファイル検索 ] というメニューに割り当てたとします。
これでは、メニューが選択されたときに、どちらのプラグインのコマンドを呼び出せばいいか判断できなくなってしまいます。
当然、ここにも何らかのトリックが必要です。

これらの問題を踏まえて、これから実装していきます。

さて、少し横道にそれますが、プラグイン可能なDLLを使用する場合、通常以下のような手順を踏んでプラグインが提供する機能にアクセスします。
まず、WindowsAPI関数の ::LoadLibrary ( ) を使用し、DLLファイルをメモリーにロードします。
次に、::GetProcAddress( ) を使用して、DLLに実装されている公開関数のアドレスを取得し、そのアドレスを使用してDLL内部の関数を呼び出します。
ここで触れたいのは、Windowsが想定しているのは、動的呼び出しでは、C言語形式のインターフェースのみ使われるということです。
しかし、すべてのインターフェースを関数形式の呼び出しで実装すると、これから徐々に複雑になってきそうです。
COMと同様の仕組みを使用すればC++のクラスを動的ロードすることも可能です。
これから使用してゆくので、最初にその方法について触れていきます。

まず、前提条件としてクラス内のメンバ関数を呼び出す場合、少なくとも関数のプロトタイプはあらかじめ知っている必要があります。
これは、GetProcAddress で関数ポインタを取得する際に、少なくとも関数の名前がわかっていないといけないというのと同じです。
実行時にクラスのメンバ関数、メンバ変数の一覧、各メンバのプロトタイプなどを実行時に知ることは(情報を何らかの形式で埋め込まない限り、たとえば別ファイルで用意する、リソースとして埋め込むなど)不可能です。
C#などの、厳格な型情報を言語レベルでサポートしている言語であればともかく、C++にはそのような仕様はありません。
(ただし、C#などであっても、実行時にプロトタイプがわかるということと、それがどういった機能を持つものなのかを呼び出し元プログラムが理解できるかはまた別問題です)
したがって、プラグインのインターフェースを基底クラスで定義し、各プラグインでは、そのクラスから派生したクラスを実装することでC++クラスをプラグイン化することが実現できそうです。
さらに、もうひとつ問題があります。
クラスのメンバ関数のアドレスの取得方法です。
GetProcAddress( ) を使用すれば、関数の先頭アドレスを取得することが可能ですが、あいにくこの関数はクラスのメンバ関数のアドレスを取ってくるには適していません。
__declspec( dllexport ) を使用して公開されたクラスのメンバ関数は、コンパイラによって装飾された名前によってDLLからエクスポートされます。
たとえば、interface1@CPluginClass のような名前になります(実際にはこれに、引数や戻り値の情報が加わるため、名前はもっと複雑になります)。
Dependency Walker というツールを使うとDLLや実行モジュールについて依存関係や、エクスポート、インポートしている関数名などを確認することができます。

しかし、OSの機能を使わず、もっと簡単にプラグイン内のクラスに含まれるメンバ関数のアドレスを取得する(つまり、プラグインとして実装されたクラス内のメンバ関数を呼び出すことができるということです)方法があります。
それは、インターフェースとして使用したい基底クラスのメンバ関数をすべて virtual として宣言することです。
virtual 関数の関数ポインタは、インスタンス生成時にクラスオブジェクト内に vtable として保持されるため、明示的にエクスポートする必要がないのです。
最後に、プラグインインターフェースクラスの作成方法です。
これは、

__declspec( dllexport ) CPluginClass *CreatePluginInstance();

などの名前で CPluginClass から派生したクラスを生成するためのインターフェースをプラグインに実装することで実現可能です。

実用的とは言い難いサンプル
あまり丁寧な作りとはいえませんが、サンプルを公開します(プロジェクトの読み込みには VC7 が必要です)。
実用的とは言い難いですが、その分シンプルな作りになっているので、ソースを理解するのはかなり容易なのではないかと思います。
ワークスペースには、メインの実行ファイルと、calc_add.dll, calc_sub.dll の2つのDLLが含まれています。
dll は 実行ファイルには静的リンクされておらず(DLLを削除してもEXEは起動します)、なおかつDLLで定義されているクラスのインターフェースを実行ファイルからコールしています。
もちろん、上で書いた理由により dllexport, dllimport などの修飾子を暮らす宣言につけるようなこともやっておりません。

ようやく準備が整いました。
実際に、Document/View をプラグイン可能なMFCベースのフレームワークを作成していきましょう。

続く

GOTO MFCを使ったPluginオブジェクトの設計と実装 TOP
制作 かみさま(水原 寛之) [連絡先]:byf03663 atmark nifty.ne.jp

バナー サークルバナー SourceForge.jp