次の方法で共有


ATL 3.0ウィンドウ クラス: 入門

Michael Park
Microsoft Corporation

1999年5月17日

要約

Active Template Library(ATL)には、Windows APIを中心とするオブジェクト指向のフレームワークを作成するクラスが含まれています。これは、Windowsプログラミングを単純化する一方で、オーバーヘッドが非常に小さいという特長を持っています。このペーパーでは、まず薄いラッパー クラスであるCWindowについて簡単に説明した後に、CWindowImplとメッセージ マップによるメッセージ処理を取り上げます。次に既存のウィンドウ クラスの機能を拡張するためのテクニックをいくつか紹介します。ここまで進むと、ATLウィンドウ クラスの機能についてかなり理解できているはずなので、一歩進んで、ATLウィンドウ クラスとは「いとこ」の関係にあるATLダイアログ ボックス クラスについて説明します。最後に、ウィンドウ スタイルや背景色などのウィンドウ属性を指定する方法について説明します。ここまで理解が進めば、読者は実用性の高いWindowsプログラムをATLを使って作成できるようになります。

キーワード

ATL、Windowsプログラミング、CWindow、CWindowImpl、メッセージ マップ、メッセージ マップ チェーン、ウィンドウのサブクラス化、ウィンドウのスーパークラス化、メッセージの反射、コンテインド ウィンドウ、CSimpleDialog、CDialogImpl、特性

はじめに

Active Template LibraryはもっぱらそのCOMサポートで知られていますが、Windowsプログラミングを単純化するクラスもいくつか備えています。これらのクラスは、ATLの他の部分と同様にテンプレートをベースにしており、オーバーヘッドが非常に低いという特長があります。この記事では、ATLを使ってウィンドウとダイアログ ボックスを作成し、メッセージを処理する方法に関する基本的な事柄を説明します。

この記事では、C++とWindowsプログラミングについての知識を前提としています。COMに関する知識は必要ありません。

CWindow

ATLの最も基本的なウィンドウ クラスは、Windows APIのオブジェクト指向ラッパーであるCWindowです。このクラスはウィンドウ ハンドルをカプセル化し、APIをラップするメンバ関数を持っています。

次に標準的なWindowsプログラミングの例を示します。

  HWND hWnd = ::CreateWindow( "button", "Click me",
WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
::ShowWindow( hWnd, nCmdShow );
::UpdateWindow( hWnd );

これと等価なコードを、ATLのCWindowクラスを使って書くと、次のようになります。

  CWindow win;
win.Create( "button", NULL, CWindow::rcDefault, "Click me",
WS_CHILD );
win.ShowWindow( nCmdShow );
win.UpdateWindow();

ATLのウィンドウ オブジェクトはウィンドウと同じものではないという点に注意してください。この文脈でのウィンドウとは、Windowsオペレーティング システムがスクリーン上の1つの領域を管理するために使用するデータのセットに過ぎません。CWindowのインスタンスなどのATLウィンドウ オブジェクトはC++オブジェクトであり、それ自身ではスクリーンやウィンドウ データ構造に関する概念は何も持っていません。その代わりに、ウィンドウ ハンドルであるm_hWndをデータ メンバとして持っています。ウィンドウ ハンドルは、CWindowオブジェクトと、それに対応するスクリーン上のウィンドウを結び付けるリンクです。

ATLウィンドウ オブジェクトとウィンドウがこのように分離されていることの結果の1つとして、CWindowオブジェクトの作成は、そのウィンドウの作成とは別に行われます。前の例では、まずCWindowオブジェクトが作成されています。

  CWindow win;

その後、そのウィンドウが作成されます。

  win.Create( "button", NULL, CWindow::rcDefault, "Howdy",
WS_OVERLAPPEDWINDOW );

また、CWindowオブジェクトを作成した後に、これを既存のウィンドウにアタッチすることも可能です。これにより、CWindowのメンバ関数を使って既存のウィンドウを操作できるようになります。CWindowは便利なラッパー関数以上のものを持っているので、これはきわめて有効な手段です。CenterWindowGetDescendantWindowなどは、Windows API以上の機能を提供します。

  HWND hWnd = CreateWindow( szWndClass, "Main window",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
// choose one of
//    CWindow win( hWnd );    // attach via constructor
// or
//    CWindow win;
//    win = hWnd;          // attach via operator=
// or
//    CWindow win;
//    win.Attach( hWnd );    // attach via Attach()
win.CenterWindow();    // now can work with win in place of hWnd
win.ShowWindow( nCmdShow );
win.UpdateWindow();

CWindow

にはHWND変換演算子もあるので、HWNDを使用するところでCWindowオブジェクトを使用することができます。

  ::ShowWindow( win, nCmdShow );  // API call expects HWND

CWindow

はウィンドウ操作をより簡単にし、オーバーヘッドがないという利点を備えています。最適化されたコンパイル済みコードは、ウィンドウ ハンドルを使った純粋なWindows APIプログラミングと同等のパフォーマンスを持っています。

残念ながら、CWindowではウィンドウのメッセージへの反応を定義することはできません。確かに、CWindowを使えばウィンドウをセンタリングしたり、非表示にしたり、さらにはウィンドウにメッセージを送ることもできますが、メッセージがウィンドウに到達したときに何が起こるかはそのウィンドウ クラスに依存し、これはウィンドウの作成時に決定されています。ウィンドウが「ボタン」クラスのインスタンスとして作成されていた場合にはボタンとして動作し、「リスト ボックス」として作成されていた場合にはリスト ボックスとして動作します。これをCWindowを使って変更する方法はありません。

しかし幸いなことに、ATLには、ウィンドウの新しい動作を指定できるCWindowImplという別のクラスがあります。

CWindowImpl

CWindowImplはCWindowを継承しているので、CWindowの便利なメンバ関数はすべて使用できますが、CWindowImplを特別なものにしているのはメッセージ処理の動作を定義できるという能力です。従来のWindowsプログラミングでは、ウィンドウのメッセージに対する応答を指定したい場合には、ウィンドウ プロシージャを書きます。ATLでは、ATLウィンドウ クラスの中に「メッセージ マップ」を定義します。

まず、次のようにCWindowImplからクラスを派生させます。

  class CMyWindow : public CWindowImpl<CMyWindow>
{

新しいクラスの名前をCWindowImplテンプレートへの引数として渡さなくてはならないことに注意してください。

クラス定義の中で、メッセージ マップを定義します。

  BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_PAINT,OnPaint)
MESSAGE_HANDLER(WM_CREATE,OnCreate)
MESSAGE_HANDLER(WM_DESTROY,OnDestroy)
END_MSG_MAP()

MESSAGE_HANDLER(WM_PAINT,OnPaint)は、「ウィンドウがWM_PAINTメッセージを受け取ったら、メンバ関数CMyWindow::OnPaintを呼び出す」という意味です。

メッセージを処理するメンバ関数を定義します。

  LRESULT OnPaint(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
LRESULT OnCreate(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
LRESULT OnDestroy(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
}; // CMyWindow

ハンドラ関数の4つの引数は、メッセージ識別子、メッセージによって内容が異なる2つのパラメータ、および、メッセージの処理が終わったのか、さらに処理が必要なのかを示すためにハンドラが使用するフラグです。この4つの引数のテストについて詳しくは、以下のメッセージ マップを参照してください。

ウィンドウがメッセージを受け取ると、メッセージ マップ エントリが先頭から検索されるので、頻繁に使用されるメッセージを最初の方に置いておくことをお勧めします。メッセージ マップの中に対応するエントリが見つからなければ、メッセージはデフォルト ウィンドウ プロシージャに渡されます。

ATLメッセージ マップは、クラス内でのウィンドウのメッセージ処理の動作をカプセル化します。また、従来のウィンドウ プロシージャのネストしたswitchおよびif文よりも可読性が高まります。

CWindowImpl

から派生したクラスに基づいてウィンドウを作成するには、CWindowImplCreateメンバ関数を呼び出します。

  CMyWindow wnd;  // constructs a CMyWindow object
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE );

このCreateは、CWindowCreateとは異なることに注意してください。CWindowオブジェクトのウィンドウを作成するときには、登録済みのウィンドウ クラスを指定する必要があります。一方、CWindowImplから派生したクラスは新しいウィンドウ クラスを定義するので、Createの呼び出しでクラス名を指定する必要はありません。

単純だが完全な例

この記事のほとんどの例はコードの一部分に過ぎませんが、ここに示す例は、ATLウィンドウ クラスを使って書かれた完全な"Hello world"プログラムです。このプログラムはATLを使用していますが、COMは使用していないので、Visual C++でATL COMプロジェクトではなくWin32アプリケーションとしてビルドすることができます。

stdafx.hに次の行を追加します。

  #include <atlbase.h>
extern CComModule _Module;
#include <atlwin.h>

hello.cppの内容:

  #include "stdafx.h"
CComModule _Module;
class CMyWindow : public CWindowImpl<CMyWindow> {
BEGIN_MSG_MAP( CMyWindow )
  MESSAGE_HANDLER( WM_PAINT, OnPaint )
  MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()

LRESULT OnPaint( UINT, WPARAM, LPARAM, BOOL& ){
  PAINTSTRUCT ps;
  HDC hDC = GetDC();
  BeginPaint( &ps );
  TextOut( hDC, 0, 0, _T("Hello world"), 11 );
  EndPaint( &ps );
  return 0;
}

LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ){
  PostQuitMessage( 0 );
  return 0;
}
};
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int )
{
_Module.Init( NULL, hInstance );
CMyWindow wnd;
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
  WS_OVERLAPPEDWINDOW|WS_VISIBLE );
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) ){
  TranslateMessage( &msg );
  DispatchMessage( &msg );
}
_Module.Term();
return msg.wParam;
}

CMyWindow

CWindowImplから派生しています。そのメッセージ マップは、WM_PAINTWM_DESTROYの2つのメッセージを処理しています。WM_PAINTメッセージが受信されると、メンバ関数OnPaintがこれを処理して、ウィンドウに"Hello world"という文字列を描画します。ユーザーがウィンドウを閉じたことを示すWM_DESTROYメッセージが受信されると、OnDestroyPostQuitMessageを呼び出して、アプリケーションのメッセージ ループを終了させます。

WinMainはCMyWindowクラスのインスタンスを作成し、標準メッセージ ループをインプリメントしています(また、 _Moduleに関連して必要となるATLの処理も若干行っています)。

メッセージ マップ

メッセージ処理マクロには、次の3つのグループがあります。

  • メッセージ ハンドラ。すべてのメッセージが対象(WM_CREATEWM_PAINTなど)。
  • コマンド ハンドラ。特にWM_COMMANDメッセージが対象(一般に、ボタンやメニュー項目などの定義済みの子ウィンドウ コントロールから送信されます)。
  • 通知ハンドラ。特にWM_NOTIFYメッセージが対象(一般に、ステータス バーやリスト ビュー コントロールなどのコモン コントロールから送信されます)。

メッセージ ハンドラ マクロ

メッセージ ハンドラ マクロには次の2つがあります。

  • MESSAGE_HANDLER
  • MESSAGE_RANGE_HANDLER

第1のマクロは、特定のメッセージをハンドラ関数にマップします。第2のマクロは、複数のメッセージの範囲をハンドラにマップします。

メッセージ ハンドラ関数は次のプロトタイプを持っています。

  LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);

uMsgはメッセージを識別し、wParamとlParamはメッセージ パラメータです(その内容はメッセージの種類によって変わります)。

ハンドラ関数は、bHandledフラグを使って、メッセージの処理を終えたかどうかを示します。BHandledがハンドラ関数の中でFALSEに設定されていると、メッセージ マップの残りの部分で、そのメッセージのためのハンドラが別にないかどうかが検索されます。これは、たとえばチェーンを使って、同じメッセージに対して個々のクラス用に複数のハンドラを用意したい場合や、メッセージに対して何らかの反応をしたいが、実際にメッセージの処理は行いたくない場合などに便利です。bHandledは関数の呼び出し前にTRUEに設定されるので、関数がbHandledを明示的にFALSEに設定しない限り、それ以上のメッセージ マップ処理は行われません。

コマンド ハンドラ マクロ

コマンド ハンドラ マクロはコマンド(WM_COMMANDメッセージ)の処理だけを行うものですが、コマンド コード、またはコマンドを送信しているコントロールの識別子をもとにしたマッピングを指定することが可能です。

  • COMMAND_HANDLER
  • COMMAND_ID_HANDLER
  • COMMAND_CODE_HANDLER
  • COMMAND_RANGE_HANDLER

コマンド ハンドラ関数は次のプロトタイプを持ちます。

  LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);

wNotifyCodeは通知コード、wIDはコマンドを送信しているコントロールの識別子、hWndCtlはコマンドを送信しているコントロールのハンドル、bHandledは前述のフラグです。

通知ハンドラ マクロ

通知ハンドラ マクロは、通知コードと、通知を送信しているコントロールの識別子に基づいて、通知(WM_NOTIFYメッセージ)を関数にマップします。これらのマクロは、コマンドではなく通知を処理するという点を除けば、前のセクションで説明したコマンド ハンドラ マクロと同等のものです。

  • NOTIFY_HANDLER
  • NOTIFY_ID_HANDLER
  • NOTIFY_CODE_HANDLER
  • NOTIFY_RANGE_HANDLER

通知ハンドラ関数は次のプロトタイプを持ちます。

  LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);

idCtrlは通知を送信しているコントロールの識別子、pnmhはNMHDR構造体へのポインタ、bHandledは前述のフラグです。

通知には、通知固有の構造体へのポインタが含まれています。たとえば、リスト ビューが通知を送信するときには、NMLVDISPINFO構造体へのポインタが通知に含まれることになります。これらの構造体はすべてNMHDRヘッダーを持っており、pnmhはこれをポイントしています。構造体の他のメンバにアクセスするには、pnmhをその構造体へのポインタにキャストしてください。

たとえば、リスト ビューからのLVN_ENDLABELEDIT通知を処理するには、親ウィンドウのメッセージ マップに次のコードを追加します。

  NOTIFY_HANDLER( ID_LISTVIEW, LVN_ENDLABELEDIT, OnEndLabelEdit)

この通知はNMLVDISPINFO構造体に追加の情報を入れています。通知ハンドラ関数は次のようなものになるでしょう。

  LRESULT OnEndLabelEdit(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
// The item is -1 if editing is being canceled.
if ( ((NMLVDISPINFO*)pnmh)->item.iItem == -1) return FALSE;
...

NMHDR

ヘッダー以外のメンバにアクセスするために、pnmhを**NMLVDISPINFO***にキャストしているところに注目してください。

既存のウィンドウ クラスへの機能の追加

既存のウィンドウ クラスに機能を追加する方法はいくつかあります。クラスがATLウィンドウ クラスである場合には、後の基本クラスのチェーンで説明するように、そこから新しいクラスを派生させることができます。これは、メッセージ マップの存在のためにいくぶん注意が必要であるとはいえ、基本的にC++の継承に他なりません。

ボタンやリスト ボックス コントロールなどの定義済みのウィンドウ クラスの能力を拡張したい場合には、スーパークラス化を行うことができます。これは、定義済みのクラスに基づいて新しいクラスを定義するものですが、メッセージ マップを追加することで、下位のウィンドウ クラスよりも高度な機能を実現することができます。

ときには、ウィンドウのクラスではなくインスタンスの動作を変更したい場合があります。ダイアログ ボックス上のエディット コントロールで何か特殊なことをやりたいような場合です。この場合には、既存のエディット コントロールをサブクラス化するATLウィンドウ クラスを作成することができます。エディット コントロールに送られたメッセージは、まずサブクラス化されたオブジェクトのメッセージ マップを通ります。

別の方法として、エディット コントロールをコンテインド ウィンドウにすることもできます。これは、自分が受け取ったメッセージを、自分を包含しているクラスのメッセージ マップに送って処理を行わせるウィンドウです。包含する側のクラスは、コンテインド ウィンドウのために特殊なメッセージ処理をインプリメントすることができます。

最後に、メッセージの反射というものがあります。これは、メッセージを受け取ったウィンドウは、処理を行わないままメッセージを送信側のウィンドウに送り返し、送信側のウィンドウがメッセージの処理を行うというものです。このテクニックにより、コントロールの自己完結性を高めることができます。

基本クラスのチェーン

ある程度の機能を持ったATLウィンドウイング クラスを定義したら、継承を利用して、そこから新しいクラスを派生させることができます。次に例を示します。

  class CBase: public CWindowImpl< CBase >
// simple base window class: shuts down app when closed
{
BEGIN_MSG_MAP( CBase )
  MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& )
{
  PostQuitMessage( 0 );
  return 0;
}
};
class CDerived: public CBase
// derived from CBase; handles mouse button events
{
BEGIN_MSG_MAP( CDerived )
  MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
END_MSG_MAP()
LRESULT OnButtonDown( UINT, WPARAM, LPARAM, BOOL& )
{
  ATLTRACE( "button down\n" );
  return 0;
}
};
// in WinMain():
...
CDerived win;
win.Create( NULL, CWindow::rcDefault, "derived window" );

しかし、この例には1つ問題があります。このプログラムをデバッガで実行すると、ウィンドウが表示されます。ウィンドウの中でクリックを行うと、デバッグ出力ウィンドウに"button down"と表示されます。これはCDerivedの動作です。しかし、CDerivedウィンドウを閉じると、CBaseWM_DESTROYメッセージを処理しており、CDerivedCBaseを継承しているのにもかかわらず、アプリケーションは実行を停止しません。

なぜでしょうか? メッセージ マップを明示的に「チェーン」する必要がある、というのがその答えです。

  BEGIN_MSG_MAP( CDerived )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
CHAIN_MSG_MAP( CBase ) // chain to base class
END_MSG_MAP()

これで、CDerivedのメッセージ マップで処理されなかったすべてのメッセージが、CBaseのメッセージ マップに送られて、そこで処理されるようになります。

クラスからその基本クラスへのチェーンが自動的に行われないのはなぜでしょうか? これは、ATLのアーキテクチャの基盤となっている多重継承のためです。一般に、多重継承が行われている場合、どの基本クラスをチェーン先とすればいいのかが判断できないため、チェーン先の指定はプログラマに任されているのです。

代替メッセージ マップ

メッセージ マップのチェーンにより、1つのメッセージ マップが複数のウィンドウ クラスからのメッセージを処理できるようになりますが、このことから問題が1つ生じます。たとえば、クラスによって異なる描画動作を行わせたいと思っていても、チェーンの中のすべてのウィンドウ クラスに対して、同じWM_PAINTハンドラが呼び出されます。この問題を解決するために、ATLは代替メッセージ マップを使用します。プログラマは、メッセージ マップを、それぞれ番号によって識別された複数のセクションに分割します。個々のセクションが1つの代替メッセージ マップとなります。

  // in class CBase:
BEGIN_MSG_MAP( CBase )
  MESSAGE_HANDLER( WM_CREATE, OnCreate1 )
  MESSAGE_HANDLER( WM_PAINT, OnPaint1 )
  ALT_MSG_MAP( 100 )
  MESSAGE_HANDLER( WM_CREATE, OnCreate2 )
  MESSAGE_HANDLER( WM_PAINT, OnPaint2 )
  ALT_MSG_MAP( 101)
  MESSAGE_HANDLER( WM_CREATE, OnCreate3 )
  MESSAGE_HANDLER( WM_PAINT, OnPaint3 )
END_MSG_MAP()

CBase

のメッセージ マップは3つのセクションから構成されています。デフォルトのメッセージ マップ(暗黙のうちに0という番号が付く)と、2つの代替メッセージ マップです(番号は100と101)。

このメッセージ マップにチェーンするときには、使用したい代替メッセージ マップの識別子を指定します。次に例を示します。

  class CDerived: public CBase {
BEGIN_MSG_MAP( CDerived )
  CHAIN_MSG_MAP_ALT( CBase, 100 )
END_MSG_MAP()
...

CDerived

のメッセージ マップはCBaseの代替メッセージ マップ100にチェーンしているので、CDerivedウィンドウがWM_PAINTメッセージを受け取ると、ハンドラ関数CBase::OnPaint2が呼び出されます。 ****

その他の種類のチェーン

基本クラスのチェーンに加えて、ATLはメンバのチェーンと動的なチェーンもサポートしています。これらのチェーンのテクニックは稀にしか使われないもので、このペーパーでは扱いませんが、簡単に説明しておくと、メンバのチェーンではメンバ変数のメッセージ マップをチェーンすることができ、動的なチェーンではメッセージ マップを実行時にチェーンすることができます。詳細については、ATLのオンライン ドキュメンテーションのCHAIN_MSG_MAP_DYNAMICCHAIN_MSG_MAP_MEMBERの項を参照してください。

ウィンドウのスーパークラス化

スーパークラス化は、ボタンやリスト ボックス コントロールなどの定義済みのウィンドウ クラスに新しい機能を追加するクラスを定義します。次の例は、ボタン コントロールをスーパークラス化して、クリックされたときにビープ音を発するボタンを定義しています。

  class CBeepButton: public CWindowImpl< CBeepButton >
{
public:
DECLARE_WND_SUPERCLASS( _T("BeepButton"), _T("Button") )
BEGIN_MSG_MAP( CBeepButton )
  MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown )
END_MSG_MAP()
LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bHandled )
{
  MessageBeep( MB_ICONASTERISK );
  bHandled = FALSE; // alternatively: DefWindowProc()
  return 0;
}
}; // CBeepButton

DECLARE_WND_SUPERCLASS

マクロは、クラスの名前("MyButton")と、スーパークラス化されるクラスの名前("Button")を宣言します。メッセージ マップは、WM_LBUTTONDOWNOnLButtonDownにマップする1つのエントリを持っています。その他のすべてのメッセージはデフォルト ウィンドウ プロシージャによって処理されます。CBeepButtonウィンドウはビープ音の他は通常のボタンと同じように動作しなくてはならないので、OnLButtonDownMessageBeepを呼び出した後にbHandledFALSEに設定して、OnLButtonDownの終了後にデフォルト ウィンドウ プロシージャにメッセージを処理させるようにしています(別の方法として、DefWindowProcを直接に呼び出すことも可能です)。

ここまでは新しいクラスを定義しただけで、実際のCBeepButtonウィンドウを作成するという処理がまだ残っています。次のクラスはCBeepButton型の2つのデータ メンバを持っています。このクラスのインスタンスを作成すると、2つのCBeepButton子ウィンドウが作成されます。

  const int ID_BUTTON1 = 101;
const int ID_BUTTON2 = 102;
class CMyWindow: public CWindowImpl< CMyWindow, CWindow, CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE> >
{
CBeepButton b1, b2;
BEGIN_MSG_MAP( CMyWindow )
  MESSAGE_HANDLER( WM_CREATE, OnCreate )
  COMMAND_CODE_HANDLER( BN_CLICKED, OnClick )
END_MSG_MAP()
LRESULT OnClick(WORD wNotifyCode, WORD wID, HWND hWndCtl,
  BOOL& bHandled)
{
  ATLTRACE( "Control %d clicked\n", wID );
  return 0;
}
LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& )
{
  RECT r1 = { 10, 10, 250, 80 };
  b1.Create(*this, r1, "beep1", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON1);
  RECT r2 = { 10, 110, 250, 180 };
  b2.Create(*this, r2, "beep2", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON2);
  return 0;
}
}; // CMyWindow

ウィンドウのサブクラス化

サブクラス化では、メッセージ マップを挿入してウィンドウのメッセージを横取りすることによって、既存のウィンドウ(通常はコントロール)の動作を変更することができます。たとえば、ダイアログ ボックスに数字以外の文字だけを受け付けるエディット コントロールを追加したいとします。このためには、エディット コントロールを宛先とするWM_CHARメッセージを横取りし、数字が入力されたことを示すすべてのメッセージを破棄します。次にこのような処理を行うクラスを示します。

  class CNoNumEdit: public CWindowImpl< CNoNumEdit >
{
BEGIN_MSG_MAP( CNoNumEdit )
  MESSAGE_HANDLER( WM_CHAR, OnChar )
END_MSG_MAP()
LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled )
{
  TCHAR ch = wParam;
  if( _T('0') <= ch && ch <= _T('9') )
    MessageBeep( 0 );
  else
    bHandled = FALSE;
  return 0;
}
};

このクラスは、WM_CHARという1つのメッセージだけを処理します。入力された文字が数字だった場合には、MessageBeepを呼び出して返ります。これにより、その文字は無視されることになります。数字以外の文字では、bHandledがFALSEに設定されるので、デフォルト ウィンドウ プロシージャがその文字の処理を行います。

次に、CNoNumEditがコントロールのメッセージを最初に受け取れるように、エディット コントロールをサブクラス化する必要があります(次の例は、ATL ダイアログ ボックス クラスのセクションで説明するCDialogImplを使用しています)。この例では、CMyDialogは、エディット コントロール(IDC_EDIT1)を持つダイアログ ボックス リソース(IDD_DIALOG1)を表しています。ダイアログ ボックスが初期化される時点で、エディット コントロールはSubclassWindowによって、数値以外の文字を受け付けるエディット コントロールに変更されます。

  class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
BEGIN_MSG_MAP( CMyDialog )
  MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
END_MSG_MAP()
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& )
{
  ed.SubclassWindow( GetDlgItem( IDC_EDIT1 ) );
  return 0;
}
CNoNumEdit ed;
};

コンテインド ウィンドウ

コンテインド ウィンドウは、メッセージの処理を行わないウィンドウです。その代わりに、自分が受け取ったすべてのメッセージを別のウィンドウに、すなわち自分のコンテナに渡します。一般に、包含される側のウィンドウは子ウィンドウであり、そのコンテナは親ウィンドウですが、そうでない場合もあります。包含する側のウィンドウは必ずしも親ウィンドウと同じウィンドウではありません。コンテナと包含される側のウィンドウの関係はC++で定義されます。つまり、包含される側のウィンドウは、コンテナ クラスのデータ メンバです。一方、スクリーン上の親ウィンドウと子ウィンドウの関係は、ウィンドウの作成時に決定されます。

コンテインド ウィンドウは、エディット コントロールなどの登録済みのウィンドウ クラスをベースにします。エディット コントロールがコンテインド ウィンドウとなると、そのコントロールに送られたすべてのメッセージが包含する側のウィンドウの中で処理されます。これにより、コンテナはエディット コントロールに通常とは異なる動作をさせることが可能になります。これはサブクラス化に似ていますが、コントロールのサブクラス化のように新しいクラスを定義する必要がないという点が異なります。前の例ではWM_CHARメッセージを処理するためにクラスCNoNumEditを定義していましたが、次の例では、包含する側のウィンドウ クラスの中にWM_CHARハンドラがあります。

  class CMyWindow: public CWindowImpl<CMyWindow>
{
CContainedWindow m_contained;
public:
CMyWindow(): m_contained( _T("edit"), this, 99 )
{
}
...

CMyWindow

はコンテナ ウィンドウ クラスです。コンストラクタはCContainedWindowメンバを次のように初期化します: コンテインド ウィンドウはエディット コントロールをベースにしており、自分に対するメッセージを代替メッセージ マップ99を使って"this"(親ウィンドウ)に転送します。

  BEGIN_MSG_MAP( CMyWindow )
MESSAGE_HANDLER( WM_CREATE, OnCreate )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
ALT_MSG_MAP( 99 ) // contained window's messages come here...
MESSAGE_HANDLER( WM_CHAR, OnChar )
END_MSG_MAP()

親ウィンドウは、作成時に、(WM_CREATEメッセージ ハンドラの中で)コンテインド ウィンドウを作成します。コンテインド ウィンドウはエディット コントロールをベースにしているので、スクリーン上ではエディット コントロールのように見えます。

  LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& )
{
RECT rc = { 10, 10, 200, 35 };
m_contained.Create( *this, rc, _T("non-numeric edit"),
  WS_CHILD|WS_VISIBLE|WS_BORDER, 0, 666 );
return 0;
}

この例では、コンテナはコンテインド ウィンドウの親ウィンドウでもあります。

コンテインド

ウィンドウがWM_CHARメッセージを受け取ると、包含する側のウィンドウのOnCharメンバ関数が呼び出されます。これはCNoNumEditの例と同じ関数ですが、コンテナのメンバになっているという点が異なります。

  LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled )
{
TCHAR ch = wParam;
if( _T('0') <= ch && ch <= _T('9') )
  MessageBeep( 0 );
else
  bHandled = FALSE;
return 0;
}
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& )
{
PostQuitMessage( 0 );
return 0;
}
};

また、コンテインド ウィンドウを使って、ダイアログ ボックス上のコントロールなどの既存の子ウィンドウ コントロールをサブクラス化することもできます。ただし、通常のサブクラス化とは異なり、サブクラス化されたウィンドウのメッセージは、包含する側のウィンドウによって処理されます。次の例では、ダイアログ ボックスがエディット コントロールをサブクラス化し、これをコンテインド ウィンドウに変更しています。ダイアログ ボックス(コンテナ)はエディット コントロールに送られたWM_CHARメッセージを処理し、数字を無視します(CDialogImplについてはATLダイアログ ボックス クラスで説明しています)。

  class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
// contained window is an edit control:
CMyDialog(): m_contained( "edit", this, 123 )
{
}
BEGIN_MSG_MAP( CMyDialog )
  MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
ALT_MSG_MAP( 123 ) // contained window’s messages come here...
  MESSAGE_HANDLER( WM_CHAR, OnChar )
END_MSG_MAP()
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& bHandled )
{
// when the dialog box is created, subclass its edit control:
  m_contained.SubclassWindow( GetDlgItem(IDC_EDIT1) );
  bHandled = FALSE;
  return 0;
}
LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled )
{
  TCHAR ch = wParam;
  if( _T('0') <= ch && ch <= _T('9') )
    MessageBeep( 0 );
  else
    bHandled = FALSE;
  return 0;
}
CContainedWindow m_contained;
};

メッセージの反射

これまでのセクションでは、ウィンドウが自分に送られたメッセージに反応するしかたを変更することで、ウィンドウ クラスの機能を拡張するテクニックをいくつか紹介しました。一方、メッセージの反射では、メッセージを送ったウィンドウがそのメッセージに反応することになります。

ユーザーがコントロールを操作すると、コントロールは一般にWM_COMMANDまたはWM_NOTIFYメッセージを送信することで、そのことを親ウィンドウに通知します。親ウィンドウはこれに応答して、何らかのアクションを実行することができます。次に例を示します。

  class CParentWindow: CWindowImpl<CParentWindow>
{
// assume that this window will have a child
// button control with an ID of ID_BUTTON
BEGIN_MSG_MAP( CParentWindow )
  COMMAND_ID_HANDLER( ID_BUTTON, OnButton )
  MESSAGE_HANDLER( WM_CTLCOLORBUTTON, OnColorButton )
  ...
  

ボタンがクリックされると、親ウィンドウにコマンドが送信され、CParentWindow::OnButtonが呼び出されます。同じように、ボタンが描画されるときには、親にWM_CTLCOLORBUTTONメッセージが送られます。CParentWindow::OnColorButtonはこれに応答して、ブラシのハンドルを返します。

ケースによっては、コントロールが送るメッセージに対して、親ウィンドウではなくそのコントロール自身に応答させた方が便利なことがあります。このために、ATLはメッセージの反射という機能を用意しています。コントロールが親にメッセージを送ると、親はメッセージを反射して、子に送り返すことができます。

  class CParentWindow: CWindowImpl<CParentWindow>
{
BEGIN_MSG_MAP( CParentWindow )
  MESSAGE_HANDLER( WM_CREATE, OnCreate )
  MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
  ...other messages that CParentWindow will handle...
  REFLECT_NOTIFICATIONS()
END_MSG_MAP()
...

親ウィンドウは、メッセージを受け取ると、自分のメッセージ マップを調べます。メッセージにマップするメッセージ マップ エントリがなければ、REFLECT_NOTIFICATIONSマクロによって、メッセージはそれを送ってきたコントロールに送り返されます。コントロールは反射されてきたメッセージのためのハンドラを次のように用意しています。

  class CHandlesItsOwnMessages: CWindowImpl<CHandlesItsOwnMessage>
{
public:
DECLARE_WND_SUPERCLASS( _T(“Superbutton”), _T(“button”) )
BEGIN_MSG_MAP( CHandlesItsOwnMessage )
  MESSAGE_HANDLER( OCM_COMMAND, OnCommand )
  MESSAGE_HANDLER( OCM_CTLCOLORBUTTON, OnColorButton )
  DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
...

反射されたメッセージのメッセージ識別子はWM_ではなくOCM_で始まることに注意してください。これにより、もともとそのコントロールを宛先としていたメッセージと、コントロールに反射されてきたメッセージを区別することができます。未処理の反射されたメッセージはDEFAULT_REFLECTION_HANDLERマクロによって処理されます。

送信元の子コントロールは、このクラスのインスタンスであるか、サブクラス化された通常のボタン コントロールでなくてはなりません。次に例を示します。

  // in CParentWindow:
CHandlesItsOwnMessages m_button;
LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& )
{
  RECT rc; // initialize appropriately
  m_button.Create( *this, rc, _T(“click me”), WS_CHILD|WS_VISIBLE );
  ...

すでにボタン コントロールがある場合は、次のようにすることもできます(たとえば、親ウィンドウがダイアログ ボックスである場合)。

  m_button.SubclassWindow( GetDlgItem(ID_BUTTON) );

次の例は、クリックされると、その名前によって指定されたWebページを開くスタティック コントロールCStaticLinkを定義しています。CStaticLinkコントロールからのすべてのメッセージは、親ウィンドウ(このケースではダイアログ ボックス。ATLダイアログ ボックス クラスを参照)によって反射され、送り返されます。CStaticLinkは、反射されたWM_COMMANDメッセージの処理に加えて、反射されたWM_CTLCOLORSTATICメッセージも処理し、クリックされたかどうかによって自分自身の色を変更します。

  #include "stdafx.h"
#include "resource.h"
CComModule _Module;
class CStaticLink : public CWindowImpl<CStaticLink> {
/*
Based on CStaticLink by Paul DiLascia, C++ Q&A, Microsoft Systems
Journal 12/1997.
Turns static controls into clickable "links" -- when the control is
clicked, the file/program/webpage named in the control's text (or
set by SetLinkText()) is opened via ShellExecute().  Static control
can be either text or graphic (bitmap, icon, etc.).
 */
public:
DECLARE_WND_SUPERCLASS( _T("StaticLink"), _T("Static") )
CStaticLink() :
  m_colorUnvisited( RGB(0,0,255) ),
  m_colorVisited( RGB(128,0,128) ),
  m_bVisited( FALSE ),
  m_hFont( NULL )
{
}
void SetLinkText( LPCTSTR szLink ) {
  USES_CONVERSION;
  m_bstrLink = T2OLE( szLink );
}
BEGIN_MSG_MAP(CStaticLink)
  // uses message reflection: WM_* comes back as OCM_*
  MESSAGE_HANDLER( OCM_COMMAND, OnCommand )
  MESSAGE_HANDLER( OCM_CTLCOLORSTATIC, OnCtlColor )
  MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) // not a reflected message
  DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) {
  if( m_hFont ) DeleteObject( m_hFont );
  return 0;
}
LRESULT OnCommand( UINT, WPARAM wParam, LPARAM, BOOL& ) {
  USES_CONVERSION;
  int code = HIWORD( wParam );
  if( code == STN_CLICKED || code == STN_DBLCLK ){
    if( m_bstrLink.Length() == 0 ){
      GetWindowText( &m_bstrLink );
    }
    if( (int)ShellExecute( *this, _T("open"),
      OLE2T(m_bstrLink), NULL, NULL, SW_SHOWNORMAL ) > 32 ){
      m_bVisited = TRUE;  // return codes > 32 => success
      Invalidate();
    }else{
      MessageBeep( 0 );
      ATLTRACE( _T("Error: CStaticLink couldn't open file") );
    }
  }
  return 0;
}
LRESULT OnCtlColor( UINT, WPARAM wParam, LPARAM, BOOL& ) {
  // notify bit must be set to get STN_* notifications
  ModifyStyle( 0, SS_NOTIFY );
  HBRUSH hBr = NULL;
  if( (GetStyle() & 0xff) <= SS_RIGHT ){
    // it's a text control: set up font and colors
    if( !m_hFont ){
      LOGFONT lf;
      GetObject( GetFont(), sizeof(lf), &lf );
      lf.lfUnderline = TRUE;
      m_hFont = CreateFontIndirect( &lf );
    }
    HDC hDC = (HDC)wParam;
    SelectObject( hDC, m_hFont );
    SetTextColor( hDC, m_bVisited ? m_colorVisited
                        : m_colorUnvisited );
    SetBkMode( hDC, TRANSPARENT );
    hBr = (HBRUSH)GetStockObject( HOLLOW_BRUSH );
  }
  return (LRESULT)hBr;
}
private:
COLORREF m_colorUnvisited;
COLORREF m_colorVisited;
BOOL m_bVisited;
HFONT m_hFont;
CComBSTR m_bstrLink;
}; // CStaticLink
class CReflectDlg : public CDialogImpl<CReflectDlg> {
public:
enum { IDD = IDD_DIALOG1 };
BEGIN_MSG_MAP(CReflectDlg)
  COMMAND_RANGE_HANDLER( IDOK, IDCANCEL, OnClose )
  MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
  REFLECT_NOTIFICATIONS()    // reflect messages back to static links
END_MSG_MAP()

LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&)
{
  CenterWindow( GetParent() );
  // a textual static control:
  s1.SubclassWindow( GetDlgItem(IDS_TEST1) );
  // a static control displaying an icon
  s2.SubclassWindow( GetDlgItem(IDS_TEST2) );
  // set the icon's link
  s2.SetLinkText( _T("https://www.microsoft.com") );
  return 1;
}
LRESULT OnClose(UINT, WPARAM wID, HWND, BOOL& )
{
  ::EndDialog( m_hWnd, wID );
  return 0;
}
private:
CStaticLink s1, s2;
}; // CReflectDlg
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
_Module.Init( NULL, hInstance );
CReflectDlg dlg;
dlg.DoModal();
_Module.Term();
return 0;
}

ATLダイアログ ボックス クラス

ここまででATLのウィンドウ クラスの機能がある程度わかったはずなので、次にダイアログ ボックスについて説明します。一般にプロジェクトには、単純なモーダルの「バージョン情報」ダイアログ ボックスから、コントロールを含んだモードレス ダイアログ ボックスまでのさまざまな種類のダイアログ ボックス リソースがいくつか含まれることになるでしょう。ATLは、アプリケーションの中からダイアログ ボックス リソースを簡単に使えるようにするCSimpleDialogCDialogImplを用意しています。

CSimpleDialog

CSimpleDialogは、テンプレートからモーダル ダイアログ ボックスを作成するクラスです。これは[OK]や[キャンセル]などの標準ボタンのためのコマンド ハンドラを持っています。CSimpleDialogは一種のメッセージ ボックスのようなものですが、ダイアログ ボックス エディタで編集して、そのレイアウトを変更できるという点が異なります。

このようなダイアログ ボックスを、たとえばユーザーがアプリケーションの[ヘルプ]メニューの[バージョン情報]をクリックしたときに表示するには、メイン ウィンドウのクラスに次のコマンド ハンドラを追加することになります。

  BEGIN_MSG_MAP( CMyMainWindow )
COMMAND_ID_HANDLER( ID_HELP_ABOUT, OnHelpAbout )
...
LRESULT OnHelpAbout( WORD, WORD, HWND, BOOL& )
{
CSimpleDialog<IDD_DIALOG1> dlg;
int ret = dlg.DoModal();
return 0;
}

ダイアログ ボックス リソースのID(IDD_DIALOG1)がCSimpleDialogテンプレートに渡されていることに注意してください。DoModalはダイアログ ボックスを表示します。ユーザーが[OK]をクリックすると、CSimpleDialogはダイアログ ボックスを閉じ、ボタンのIDを返します(CSimpleDialogIDOKIDCANCELIDABORTIDRETRYIDIGNOREIDYES、およびIDNOのコマンド ハンドラを持っています)。

CDialogImpl

CSimpleDialogは単純なモーダル ダイアログ ボックスしか扱えません。これよりも複雑なダイアログ ボックス、あるいはモードレス ダイアログ ボックスを使いたい場合には、CDialogImplを使用します(CSimpleDialogは実際にはCDialogImplの制限付きバージョンです)。

モードレス ダイアログ ボックスをインプリメントしたい場合には、CDialogImplからクラスを派生させる必要があります。CWindowImplの場合と同様に、CDialogImplへのテンプレート引数として、新しいクラスの名前を渡します。

  class CMyModelessDialog: public CDialogImpl<CMyModelessDialog>
{

CSimpleDialog

とは異なり、テンプレート引数としてダイアログ ボックス リソース識別子を渡す必要はありません。ただし、このクラスをダイアログ ボックス リソースに「接続」する必要はあります。これは、クラスの中でenumを定義することによって行います。

  public:
enum { IDD = IDD_DIALOG1 };

次にメッセージ マップを宣言します。

  BEGIN_MSG_MAP( CMyDialog )
MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
MESSAGE_HANDLER( WM_CLOSE, OnClose )
...
END_MSG_MAP()

ハンドラ関数定義はプログラマが自由に行うことができますが、ここではモードレス ダイアログ ボックスをインプリメントするので、WM_CLOSEメッセージへの応答としてDestroyWindowを呼び出す必要があります。

  LRESULT OnClose( UINT, WPARAM, LPARAM, BOOL& )
{
DestroyWindow();
return 0;
}
...
}; // CMyModelessDialog

このダイアログ ボックスをスクリーン上に表示するには、クラスのインスタンスを作成し、そのCreate関数を呼び出す必要があります。

  CMyModelessDialog dlg;
dlg.Create( wndParent );

ダイアログ ボックス リソースがWS_VISIBLEスタイルを持っていない場合には、ダイアログ ボックスを可視状態にする必要があります。

  dlg.ShowWindow( SW_SHOW );

次の例は、ユーザーから文字列を受け取るモードレス ダイアログ ボックスと、文字列を表示するメイン ウィンドウを持っています。ダイアログ ボックスはエディット ボックスとボタンを持っています。ボタンがクリックされると、ダイアログ ボックスはそのオーナー ウィンドウのDoSomethingメンバ関数を呼び出し、エディット ボックスの中のテキストを渡します。オーナー ウィンドウはスーパークラス化されたリスト ボックス コントロールで、DoSomethingはそのリストに新しい文字列を追加します。

  #include "atlbase.h"
CComModule _Module;
#include "atlwin.h"
#include "resource.h"
class CMyWindow: public CWindowImpl<CMyWindow>
{
public:
DECLARE_WND_SUPERCLASS( "MyWindow", "listbox" )
BEGIN_MSG_MAP( CMyWindow )
  MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& )
{
  PostQuitMessage( 0 );
  return 0;
}
void DoSomething( LPCTSTR s )
{
  SendMessage( LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(s) );
}
};
class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
BEGIN_MSG_MAP( CMyDialog )
  COMMAND_ID_HANDLER( IDC_BUTTON1, OnButton )
  MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
  MESSAGE_HANDLER( WM_CLOSE, OnClose )
END_MSG_MAP()
LRESULT OnButton(WORD, WORD, HWND, BOOL&)
{
  char buf[100];
  m_ed.GetWindowText( buf, 100 );
  m_owner.DoSomething( buf );
  return 0;
}
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& )
{
  m_owner.Attach( GetParent() );
  CenterWindow( m_owner );
  m_ed = GetDlgItem( IDC_EDIT1 );
  return TRUE;
}
LRESULT OnClose( UINT, WPARAM, LPARAM, BOOL& )
{
  DestroyWindow();
  m_owner.Detach();
  return 0;
}
CMyWindow m_owner;
CWindow m_ed;
};
CMyDialog dlg;
int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
_Module.Init( NULL, hInstance );
CMyWindow win;
win.Create( NULL, CWindow::rcDefault, _T("modeless dialog test"),
  WS_OVERLAPPEDWINDOW|WS_VISIBLE );
dlg.Create( win );
dlg.ShowWindow( SW_SHOW );
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) ){
  if( !IsWindow(dlg) || !dlg.IsDialogMessage( &msg ) ){
    DispatchMessage( &msg );
  }
}
_Module.Term();
return 0;
}

ウィンドウ クラス情報の指定

このペーパーの大部分では、ウィンドウ クラスの動作、すなわちウィンドウがメッセージにどのように反応するかということについて説明してきました。ウィンドウ クラスは、その動作の他にも、スタイル、クラス名、背景色、カーソルなどの重要な属性を持っています。このセクションでは、ATLがこれらの属性を指定するために用意しているマクロの使用方法を示します。

ウィンドウ特性によるスタイルの指定

これまでの例では、ウィンドウ スタイルはCreateの呼び出しの中で指定されていました。

  CMyWindow wnd;
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE );

プログラマがウィンドウ スタイル(または拡張ウィンドウ スタイル)を指定しなかった場合、ATLはデフォルト値を使用します。これらのデフォルト値は、ウィンドウ クラスの特性として定義されています。デフォルトの特性は、次のように定義されているCControlWinTraitsです。

  typedef CWinTraits<WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN
|WS_CLIPSIBLINGS, 0> CControlWinTraits;

CWinTraits

は特性のテンプレートで、ウィンドウ スタイルと拡張ウィンドウ スタイルの2つの引数を取ります。 ****

CWindowImpl

の定義では、CControlWinTraitsがデフォルト テンプレート パラメータとして使用されています。

  template <class T,
          class TBase = CWindow,
          class TWinTraits = CControlWinTraits>
class CWindowImpl : public ...

したがって、デフォルトでは、CWindowImplから派生したウィンドウは、子と兄弟をクリップする可視状態の子ウィンドウです。

プログラマ独自のウィンドウ特性を定義することも可能です。

  typedef CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE,0>
MyTraits;

その後、CWindowImplからクラスを派生するときに、ウィンドウ特性を指定します。

  class CMyWindow: public CWindowImpl<CMyWindow,CWindow,MyTraits>
{...};

より直接的に指定するには、次のようにします。

  class CMyWindow: public CWindowImpl<
CMyWindow,
CWindow,
CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE,0> 
>
{...};

派生クラス、基本クラス(CWindow)、および特性クラスの3つのテンプレート引数をすべて指定しなくてはならないことに注意してください。

CMyWindow

ウィンドウは、「可視状態のオーバーラップ ウィンドウ」というデフォルト スタイルを持っているので、Create呼び出しではこれらのスタイルを省略することができます。

  CMyWindow wnd;
wnd.Create( NULL, CWindow::rcDefault, _T("Hello") );
// style: WS_OVERLAPPEDWINDOW|WS_VISIBLE

もちろん、特性をオーバーライドすることもできます。

  ovwnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW ); // not visible

ウィンドウ特性には拡張スタイルを追加することもできます。

  class CClientWindow: public CWindowImpl<CClientWindow, CWindow,
CWinTraits< WS_OVERLAPPEDWINDOW|WS_VISIBLE, WS_EX_CLIENTEDGE > >
{...};

DECLARE_WND_CLASS

DECLARE_WND_CLASSマクロを使うと、ウィンドウ クラスの名前だけを定義することができます。

  DECLARE_WND_CLASS(“my window class”);

これは次のコードと等価です。

  DECLARE_WND_CLASS_EX(
“my window class”,
CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, // default style
COLOR_WINDOW                      // default color
);

DECLARE_WND_CLASS_EX

DECLARE_WND_CLASS_EXマクロを使うと、クラスの名前を指定し、そのスタイルと背景色を指定することができます。

  class CMyWindow: public CWindowImpl<CMyWindow>
{
public:
DECLARE_WND_CLASS_EX(
  “my window class”,       // class name
  CS_HREDRAW|CS_VREDRAW,   // class style
  COLOR_WINDOW             // background color
);
BEGIN_MSG_MAP(CMyWindow)
  ...

クラス名は、クラスの登録時に使用された名前です。名前を指定しなければ、ATLが自動的に名前を生成しますが、Spy++などのツールを使用したときに、"ATL:00424bd0"などのわかりにくい名前よりは、独自の名前を付けた方が便利です。

クラス スタイルはクラス スタイル フラグとのビットORが取られます。

背景色は標準システム カラーのいずれかの色です。

CWndClassInfo

ウィンドウ クラスは、**DECLARE_WND_**マクロよりも細かくカスタマイズすることができます。DECLARE_WND_CLASSの定義を見ると、CWndClassInfo構造体と、この構造体を返す関数の定義が含まれていることがわかります。

  #define DECLARE_WND_CLASS(WndClassName) \
static CWndClassInfo& GetWndClassInfo() \
{ \
static CWndClassInfo wc = \
{ \
  { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, \
    StartWindowProc, \
    0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, \
    WndClassName, NULL }, \
  NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \
}; \
return wc; \
}

CWndClassInfo

は、カスタマイズの幅を大きく広げる構造体です。次にその内容を示します。

  struct CWndClassInfo
{
struct WNDCLASSEX
{
  UINT        cbSize;
  UINT        style;
  WNDPROC     lpfnWndProc;
  int         cbClsExtra;
  int         cbWndExtra;
  HINSTANCE   hInstance;
  HICON       hIcon;
  HCURSOR     hCursor;
  HBRUSH      hbrBackground;
  LPCSTR      lpszMenuName;
  LPCSTR      lpszClassName;
  HICON       hIconSm;
} m_wc;
LPCSTR m_lpszOrigName;
WNDPROC pWndProc;
LPCSTR m_lpszCursorID;
BOOL m_bSystemCursor;
ATOM m_atom;
CHAR m_szAutoName[13];
ATOM Register(WNDPROC* p);
};

たとえば、ウィンドウのカーソルを指定するには、m_lpszCursorIDにカーソルの名前を設定します。システム カーソルの場合はm_bSystemCursorTRUEに設定し、そうでなければFALSEに設定します。DECLARE_WND_CLASSは、この2つのメンバをそれぞれIDC_ARROWTRUEに設定しています。**DECLARE_WND_**マクロにはこれらのデフォルト値を変更する手段が用意されていないので、プログラマが自分で変更しなくてはなりません。

  class CMyWindow: public CWindowImpl<CMyWindow>
{
public:
static CWndClassInfo& GetWndClassInfo()
{
  // a manual DECLARE_WND_CLASS macro expansion
  // modified to specify an application-defined cursor:
  static CWndClassInfo wc =
  {
    { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
      StartWindowProc,
      0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL,
      "MyWindow", NULL },
    NULL, NULL, MAKEINTRESOURCE(IDC_CURSOR1), FALSE, 0, _T("")
  };
  return wc;
}
...

結論

ATLは、単純かつエレガントで強力なWindowsプログラミング モデルを提供します。ラッパー関数、メッセージ マップ、およびマクロといった便利な機能に加えて、チェーン、ウィンドウのサブクラス化とスーパークラス化、コンテインド ウィンドウ、および反射メッセージなどのテクニックによって、ウィンドウおよびダイアログ ボックス クラスのデザインとインプリメンテーションに大きな柔軟性が生まれます。最も印象的なのは、ATLがこのようなパワーと柔軟性を提供するだけでなく、メモリと実行時間のオーバーヘッドをほとんど持たないという事実でしょう。