いきなり、MVVM もどきの __event/__hook に持って行くと移行の方法がわからなくなるので、それ以前の話を残しておきます。
■現状は同じ
- GridView と GraihpcsView の 2つの View がある。
- GridView の項目をチェックすると、GraphicsView のある点が反転する。
- 逆に GraphicsView のある点を反転させると、GridView の項目のチェックが反転する。
という、2つのViewの相互伝播の機能を実現するとして、__event/__hook なしでどのように書いてある/書いたのかというと…
■実装の仕方は2つある
実装の仕方は2つあります。オブジェクト指向的に GridView, GraphicsView に適当なインターフェース(OnClick, OnCheck, OnChanged)を View に配置させて、相互に呼び出しをします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Model ; class GraphicsView { void OnClick(); void Onchanged( Model *model ); }; class GridView { void OnChecked(); void OnChanged( Model *model ); }; GraphicsView *graphicsView; GridView *gridView; void GraphicsView::OnClick() { gridView->OnChanged( model ); } void GridView::OnCheck() { graphicsView->OnChanged( model ); } |
とします。一見、これで平和なような気がしますが、これに新たに View を加えると大混乱に陥るのですよ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Model ; class GraphicsView { void OnClick(); void Onchanged( Model *model ); }; class GridView { void OnChecked(); void OnChanged( Model *model ); }; // 新しい View を追加 class OtherView { void OnChanged( Model *model ); }; GraphicsView *graphicsView; GridView *gridView; OtherView *otherView; void GraphicsView::OnClick() { gridView->OnChanged( model ); otherView->Onchanged( model ); // add; } void GridView::OnCheck() { graphicsView->OnChanged( model ); otherView->Onchanged( model ); // add; } |
こんな風に、View 同士の関係が密着過ぎるので、何か変更があるたびにあちこちの実装に手を入れることなります。
全然「疎結合」ではないですよね。
■疎結合を実現させるために、通知を一元管理する
オブジェクト指向の「隠蔽化」≒「疎結合」を実現させるために、View 同士を密着させずにメッセージ処理をするクラスをワンクッション置きます。
ここでよく使われるのが、MainForm クラスですね。大抵が View の親ウィンドウになるので this->GetParent() で拾ってこれるため、((MainForm*)this->GetParent())->OnChange(…) という呼び出しが可能なのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Model ; class GraphicsView { void OnClick(); void Onchanged( Model *model ); }; class GridView { void OnChecked(); void OnChanged( Model *model ); }; class MainForm { void OnChanged( Model *model ); }; GraphicsView *graphicsView; GridView *gridView; MainForm *mainForm; void GraphicsView::OnClick() { mainForm->OnChanged( model ); } void GridView::OnCheck() { mainForm->OnChanged( model ); } void MainForm::OnChanged( model ) { graphicsView->OnChanegd( model ); gridView->OnChanegd( model ); } |
一度、MainForm にメッセージを通知してから、2 つの View に通知をしています。これで、OtherView が追加されても、MainForm::OnChanged の中身を変更するだけで済みます。
が、これには問題があって、MainForm は、View の型を知らないといけないのです。GraphicsView, GridView, OtherView と追加されるたびに、MainForm は各 View の #include をしなければいけません。これが結構面倒だし、View のヘッダを修正するたびに MainForm のコンパイルが走ります。
そうそう、MainForm が肥大化するというのは、考えてみれば VM が肥大化する、Controller が肥大化するのと同じ現象です。
■SendMessage を使って疎結合にする
更に疎結合にするために、window handle, WinProc, SendMessage を使います。
「更に疎結合」という書き方をしましたが、実は、これが当時普通に書いていたやり方です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | class Model ; class GraphicsView { void OnClick(); void Onchanged( Model *model ); }; class GridView { void OnChecked(); void OnChanged( Model *model ); }; class MainForm { void OnChanged( Model *model ); }; CWnd *graphicsView; CWnd *gridView; MainForm *mainForm; void GraphicsView::OnClick() { mainForm->OnChanged( model ); } void GraphicsView::WinProc( int msg, WPARAM wparam, LPARAM lparam ) { switch ( msg ) { case WM_USER_ON_CHANGE: this ->OnChanged((Model*)lparam); break ; } } void GridView::OnCheck() { mainForm->OnChanged( model ); } void GridView::WinProc( int msg, WPARAM wparam, LPARAM lparam ) { switch ( msg ) { case WM_USER_ON_CHANGE: this ->OnChanged((Model*)lparam); break ; } } void MainForm::OnChanged( model ) { graphicsView->SendMessage( WM_USER_ON_CHANGE, NULL, model ); gridView->SendMessage( WM_USER_ON_CHANGE, NULL, model ); } |
View 自体は window handle(HWND)を持っているので、SendMessage の対象にできます。SendMessage は昔から Windows アプリで良く使うメッセージで、いわゆる Command パターンの祖先みたいなものです。メッセージ ID と 2 つのパラメータを渡すのです。この使い方は、.NET の XXXArge クラスにも継承されています。
これで、無理矢理ではありますが、MainForm は GraphicsView などの View の型から自由になります。
上の例のように、graphicsView を CWnd だけで受けるようにしておけば、GraphicsView のヘッダファイルを include しなくて良くなります。
で、この GraphicsView::WinProc の書き方は定番なので(window アプリのイベント駆動という点で)、C++ でのマクロが用意されています。
テクニカル ノート 6: メッセージ マップ
http://msdn.microsoft.com/ja-jp/library/0812b0wa.aspx
にあるように、ON_MESSAGE を使って簡単に書けるのですよ。便利ですね~ < ホントかよッ!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | class Model ; class GraphicsView : CWnd { void OnClick(); protected : //{{AFX_MSG(GraphicsView) afx_msg void OnChanged(WPARAM wparam, LPARAM lparam); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; class GridView { void OnChecked(); protected : //{{AFX_MSG(GridView) afx_msg void OnChanged(WPARAM wparam, LPARAM lparam); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; class MainForm { protected : //{{AFX_MSG(MainForm) afx_msg void OnChanged(WPARAM wparam, LPARAM lparam); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; #define WM_USER_ON_CHANGE (WM_USER+1) CWnd *graphicsView; CWnd *gridView; MainForm *mainForm; BEGIN_MESSAGE_MAP(GraphicsView, CWnd) ON_MESSAGE(WM_USER_ON_CHANGE, OnChanged) END_MESSAGE_MAP() void GraphicsView::OnClick() { mainForm->SendMessage( WM_USER_ON_CHANGE, NULL, model ); } BEGIN_MESSAGE_MAP(GridView, CWnd) ON_MESSAGE(WM_USER_ON_CHANGE, OnChanged) END_MESSAGE_MAP() void GridView::OnCheck() { mainForm->SendMessage( WM_USER_ON_CHANGE, NULL, model ); } BEGIN_MESSAGE_MAP(MainForm, CWnd) ON_MESSAGE(WM_USER_ON_CHANGE, OnChanged) END_MESSAGE_MAP() void MainForm::OnChanged( model ) { graphicsView->SendMessage( WM_USER_ON_CHANGE, NULL, model ); gridView->SendMessage( WM_USER_ON_CHANGE, NULL, model ); } |
ほら、ON_MESSAGE を使うことによって、WinProc の switch-case が無くなってプログラムコードがすっきりしましたねッ!!!(これは当時、本当にあった「謳い文句」だった)。
まあ、実際にはボタンのクリックイベント(OnClick)などは、VC++ のクラスウィザードやリソースエディタで自動生成してくれるので、こんな風にカスタムにやらない限りはあまり手を付けない分野なのですが…大変といえば大変ですね。実は、C# のイベントハンドリングは自動生成されたコードで、各コントロールのデリゲートと結び付けられているので、構造は似たようなものです。そのあたり、デザイナが仕切ってくれるので楽なのです。かつ、partial でソースが分割されているので、自動生成されたコードと自前のコードが混在されてなくてややこしくないのです。C++ の場合は(何故か C++/CLI もそうなのですが)、自動生成コードと手打ちのコードの混在が激しくて面倒なんですね。分けてくれるとコーディングが楽なんですが… C++/CX でも分離はされていないので、残念ながら望みは薄いでしょう。
■MFC appliceation は先行きどうするべきか?
先ごろ「MFC is dead」って形で、新しい MFC を開発しないということが決まったようなので(まあ、当然の帰着。限られたリソースは適切に配置するということで)、MFC アプリケーションのデザイナが、何らかの形で「改善」されることは望みが薄いのです…と言いますか、この MFC の構造は VC++5 の時代から続いているものです。途中で、ATL/WTL という形でテンプレートを使った windows アプリの提案もなされていたのですが、立ち消えています。いや、存続はしているのですが、MFC を主に使ってきた私には使いづらくて。COM 関係は ATL で作ると便利なんですけどね。
当然、これからの windows アプリケーションは、.NET が主流になる訳で、C#/VB で書くのが普通になります(現在でも普通そうします)。metro style の場合、C++/CX という選択肢もありますが、DirectX を触らなければ、C#/VB のほうが書きやすいでしょう。それに、C++/CX は、言語自体が COM を使いやすいように「勝手に」拡張されているので、WinRT に即したアプリは作り易いですね。
そういう意味では、MFC/C++ の組み合わせを使う場面は、COBOL 並にレガシーなわけで(最近は、Java の Enterprise もレガシー扱いですが)、レガシーはレガシーなままで使うというのもひとつの方法です。しかし、今回、ソースコードが 1.5GB 以上の C++ コードを改修していると(ステップ数は怖くて勘定できないw)、当時の古風なオブジェクト指向(あるいは誤ったオブジェクト指向)に即してレガシーなコーディングで追加をするよりも、できるならば、C# などで培われたイベントの仕組みや LINQ もどきの実装などを汲み入れた形で MFC アプリケーションに追加していくのが良かろうというのが今の私の考えです。
boost を使って、modarn c++ で書き換えることも考えたのですが、どうもこのコード、テンプレートプログラミングをやっても良く/楽にならなそうなんですよね。どうも window メッセージ回り、View 廻りの取り回しで苦労している感じが大。
■MFC applicaiton は何と組み合わせるのか?
MFC に限らずなのですが、OpenCV や Fortran を使うとインターフェースは C++ 一択になります。OpenCV を C# で包む実装もあるにはあるのですが、内部的に新しい視覚認識のコードを作ろうとすると C++ でのコーディングは外せません。また、Fortran の場合、インターフェースがポインタなので、頑張れば C# の unsafe を使ってできないこともないのですが、作業量的に辛いものがあります。
また、既存のコードが、C言語の構造体を使っているとかポインタを前提にして作られていると、C# only ではお手上げなのです。
そうなると、仲立ちとしての C++/CLI が問われるわけで、このあたりきちんと「レイヤー」を分けて作ると、レガシーな C++, Fortran ライブラリを活用できます。
- View は適宜、C#/VB で作成する。
- 内部ロジック、レガシーコードの移行は、C++ のままで行う。
- つなぎを、C++/CLI で書く。
「つなぎ」という言い方をしてますが、C言語の構造体やC++のクラスをそのままC#に直すのは、パフォーマンス的に問題があります。OpenCV の C# インターフェースも各要素を C# 内で扱おうとするとパフォーマンスが低下するでしょう(実測していませんが…なので後で試してみますか)。しかし、View に近いところや、パラメータの部分は C# で扱う方が便利だし、作業も効率的です。そのあたりも踏まえた上で「レイヤー」を意識して「つなぎ」しないといけません。
あとは、そこそこの画面(View)を、C++ だけでさっくりと作れるならばそれに越したことはないのです。ここで、例に出している GridView、GraphicsView は複雑な View なので内部的には C# にするか、DiretX か OpenGL 部分を再構築するか、の選択肢になるのですが、CButton や CEdit などの設定画面をさっくりと作れるようになる、さっくりとカスタマイズできるようになることが必要かと。まぁ、そのあたりはぼちぼちとひと夏かけて考えていきますか。多分。