WTL メモ

http://toshi.my.land.to/wtl/memo/
* トップ ページ - WTL - メモ :: このページについて / メモ一覧

このページについて

WTL を利用するにあたって、発見したこと、注意すべきことなどをまとめた備忘録代わりのメモです。
リファレンスと同様、ATL 9.0 および WTL 8.1.9127 を前提としています。
なお、文章の表記が紛らわしかったり分かりづらかったり納得いかないものである部分もあるかと思いますが、ご了承ください。
また、内容が ATL に絡んでくることもありますが、もはや必然と思ってスルーしていただけると幸いです。

メモ一覧

まとめてあるメモの一覧です。

最新の WTL でも過去の _Module を使っている理由
CFrameWindowImplBase と CFrameWindowImpl
リフレクションの仕組み
WTL における MDI


最新の WTL でも過去の _Module を使っている理由

ATL 7.0 以降は、CComModule _Module の代わりに CAtlBaseModule / CAtlComModule / CAtlWinModule / CAtlModule などの
モジュール クラスを利用することが推奨されているようです。

参照 : ATL モジュール クラス

しかし、最新の WTL でもってしてプロジェクトをウィザードで作成しても、
CComModule を継承した (CAppModule|CServerAppModule) _Module が定義されています。
憶測ですが、次のような理由からと思われます。

CMessageLoop::AddMessageFilter メソッドなどでメッセージ ループをカスタマイズする場合、通常 OnCreate ハンドラ等で行います。
これは WindowImpl 系を継承しているクラスに CMessageFilter 等を多重継承させるパターンにおいて、最も分かりやすいタイミングだからではないでしょうか。
するとメッセージ ハンドラでメッセージ ループにアクセスできる手段が必要になります。
通常 CMessageLoop は _tWinMain とかそのあたりの関数のローカル変数になっていますので、
こういった場合にグローバル オブジェクトに登録してあると容易に取り出せるわけですね。
CMessageLoop は WTL 特有なので、必然的にグローバル オブジェクトも WTL 特有のものが必要になります。
そうなると CAppModule のようなクラスが適役だということなのでしょう。

今や CComModule は互換性のために残されている過去の遺物ですので、使うことに対してためらいがあることも否めませんが、
プロジェクトのテンプレートがそうなっている以上、どうしようもありませんので使いましょう。
というか、昔から WTL がこのような手段を取ってきたのであれば、それこそ互換性を重視した結果ともいえますね。

メッセージ ハンドラに限らずあらゆるスコープでメッセージ ループにアクセスできる手段、それが _Module なのです。


CFrameWindowImplBase と CFrameWindowImpl

なんで別々にして継承させているのでしょうか。
メッセージ ハンドラも ImplBase の方にたくさんあって、Impl には OnChevronPushed / OnReBarAutoSize / OnSize のみです。
ポイントとして、Impl の方は ImplBase の Create メソッドをオーバーライドして、デフォルト引数によって呼び出しを簡易にしていますね。
更に CreateEx メソッドもあり、共通リソース ID を利用できるようになっています。
普通に使う分には Impl を継承すればいいことはもはや自明ですが...。

と思ったら見つけました。CMDIFrameWindowImpl と CMDIChildWindowImpl。
これらはそれぞれ ImplBase の方を継承しています。
つまり、フレーム ウィンドウとして最低限必要な機能を ImplBase に集約して、
そこから継承し、SDI なら CFrameWindowImpl、MDI なら CMDI(Frame|Child)WindowImpl にそれぞれ独自の機能を持たせたわけですね。
ソースを読んでいれば分かることでした...。


リフレクションの仕組み

コントロールのオーナードロー等の処理を、通知を受ける親ウィンドウではなくコントロール自身に処理させることのできるリフレクション。
具体的にどのような処理をしているのかを詳しく知りたいので、その動きを追ってみました。

前提として、
・ウィンドウ上にあるボタンから WM_DRAWITEM メッセージを受け取ったとします。
・このボタンはシステムの Button を DECLARE_WND_SUPERCLASS マクロによってスーパークラス化したものです。
・当然ながら、そのクラスは COwnerDraw< そのクラス > を継承しています。

1. ウィンドウのメッセージ マップにおいて、WM_DRAWITEM メッセージに対応するハンドラはないので(むしろあったらリフレクションができません)、
処理が REFLECT_NOTIFICATIONS マクロまで到達します。
2. そのマクロでは、ATL::CWindowImplRoot::ReflectNotifications メソッドを呼び出します。
3. そのメソッドでは、リフレクトすべきメッセージを判別し、送信元へ SendMessage します。つまり送り返します。
その時の uMsg は、OCM__BASE + < 元のメッセージ > です。リフレクトとそうでないものの区別ですね。
ちなみにこの OCM__BASE は、OleCtl.h に定義されています。
4. 送り返されたメッセージは、メッセージ マップの CHAIN_MSG_MAP_ALT マクロによって COwnerDraw のメッセージ マップへ飛ばされます。
なお、DEFAULT_REFLECTION_HANDLER マクロもその下に記述しておくことで、
スルーされたリフレクション メッセージを元のメッセージに直してもらって DefWindowProc に処理させることができます。
5. COwnerDraw のメッセージ マップで、メッセージは ALT_MSG_MAP マクロまで到達します。
あとは OnDrawItem ハンドラに処理が行くので、そこで自分でオーバーライドした DrawItem を呼んでくれます。

このような仕組みのお陰で、COwnerDraw クラスを親ウィンドウが継承しても、コントロール自身が継承してもオーナードローできるわけです。
個人的には、コントロール自身に処理させる方が、分かりやすい気がしますね。


WTL における MDI

WTL とか関係なく MDI の挙動はなかなか面白いものがありますが、ここでは WTL が MDI をどのように扱っているかを調べてみました。

・CMDIWindow の存在
WTL には ATL::CWindow を継承した CMDIWindow クラスがあります。
これは ATL::CWindow の振る舞いに加え、MDI 特有の振る舞いを実装したクラスです。
CMDIWindow から派生しているのは CMDIFrameWindowImpl / CMDIChildWindowImpl です。つまりフレームとチャイルドの両方が同じクラスから派生しているわけです。
これってどっちも役割がだいぶ違うはずですが、なんでこんなことができているのかというと、「MDI クライアント ウィンドウ」があるからです。

CMDIWindow は、m_hWndMDIClient というメンバとして MDI クライアント ウィンドウのハンドルを持っています。
MDI に関わる処理の大部分は、この MDI クライアント ウィンドウが請け負っていますので、このメンバさえ持っていればフレームだろうがチャイルドだろうが同じやり方(勿論 SendMessage)でクライアントに頼むことができますので、MDI に関わる処理の部分を共通化できるわけですね。
とにかく MDI のキモは MDI クライアント ウィンドウだと言っても過言ではありません。

・メニューの扱われ方
MSDN で MDI のリファレンスを見ていると、"frame window menu" と "window menu" という言葉が出てきます。

frame window menu は、MDI フレーム ウィンドウが持っているメニューです。
でも、持っていることは持っていますが、それが表に出てくる機会はなかなかありません。
なぜなら、チャイルド ウィンドウが 1 つでもあれば、それが持っているメニューがフレームに設定されてしまうからですね。
フレーム自体が持っているメニューは、チャイルドがないときにだけ表示されることになります。

window menu は、いわゆる [ウィンドウ(W)] メニューです。
重ねて表示とか並べて表示とかができたり、チャイルドの一覧が表示されてそこから選べたりするあのメニューのことです。

このチャイルドの一覧については、自分で管理するのはめんどくさいので、MDI クライアントに任せることができます。
WTL を使っていれば、その任せる処理も WTL に任せることができます。
_WTL_MDIWINDOWMENU_TEXT を TEXT("ウィンドウ(&W)") とでも定義しておけば、名前がその文字列になっているメニュー項目の下に、チャイルドの一覧の項目を追加してくれます(10 個以上ある時は [その他のウィンドウ(M)...] という項目も追加してくれます)。
なお、この定義は atlframe.h がインクルードされる前に行っておく必要があります。

注意点として、WTL のコードを見る限りでは右から 2 番目の項目のみが対象となっているようですので、右に [ヘルプ(H)] のような項目が 1 つだけ存在していないとダメっぽいです。

・チャイルド ウィンドウの作成
チャイルド ウィンドウを作るだけなら WM_MDICREATE メッセージをクライアントに送るだけでできますが、WTL ではそういうメソッドを実装していません。
なぜかって、そしたら誰がそのウィンドウを管理するんだ、ということになるからですね。
なので、チャイルドのためのクラスを CMDIChildWindowImpl から派生させて、それを new して、クライアントを親ウィンドウにして Create するという処理が必要になります。
delete のタイミングは OnFinalMessage がベストでしょう。


Last Modified : 2010/04/04 (Sun.) 14:37:56

Memorian Version 0.1.0.0
By Toshi

ページの先頭へ戻る
WTL へ戻る
トップ ページへ戻る

* トップ ページ - WTL - メモ :: このページについて / メモ一覧
http://toshi.my.land.to/wtl/memo/
(C) 2005 - 2010 Toshi, All Rights Reserved.