ATLを使わないWTLもどき

正式名称 Natsuhaze Windows Class Library (Nwcl)

written by shinsuke okazaki

最新版はなつはぜソフトウェアから



ATLもWTLも知らない人には何のことやらさっぱりわからないと思いますが、このライブラリはWindowsのユーザーインターフェイス開発用C++クラスライブラリです。

フリーソフトです。(vectorもどきを内部で使っています)。WTLのコードを一部拝借しています。といっても著作権を主張するにはあまりも自明なコントロールクラスのメンバ関数のみです。
WTLのライセンスが難解なのですが多分商利用も問題ないでしょう。著作権がらみで問題と思われる方はWTLのメンバー関数を使うのではなく、Winodws SDK(ヘッダファイル)にある、Windowsのコントロール制御マクロ(後述)を使えばいいですが、1〜2行程度のものばかりなので内容は実質同じものでしょう。
 一応お決まりの言葉ですが、どんな使い方をしても構いませんが使用したことによる損害は補償しません。無保証です。

開発経緯

時々Windows Template Library(WTL)を使って開発しているのですが、どうも最新版のVisualStudio Express(2010)だとうまくビルドしてくれない。WTLはATL(Active Template Class Library)を必要とするのですが、このATLのバージョンが低いとコンパイルがうまくいかないのです。
 結局WTLは自由に使えるといいながら、製品版の一部(ATL)を使うことが必須なライブラリで、真に自由なライブラリではないのです。

WTLを使うためにわざわざ製品版の最新のVisualStudioを買うのも馬鹿らしいので、ちょっと作ってみるかと思ってこしらえたものが、このライブラリです。だから製品版のVisualStudioを持っている人には意味のないライブラリです。貧乏人のためのライブラリといったらいいでしょうか?(笑)

MFC(Microsoft Foundation Class Library)で開発している人でも簡単なGUIしか作らないのであればNwclでも十分対応できると思いますし、データ交換変数に関して言えばMFC以上です。

---------------
特徴:

1.現在ダイアログとプロパティーシートのみ対応。モードレスダイアログは不対応(私はマルチスレッドを使って同じことを実現しているので必要を感じない)。
 まあGUI作るといえば、ほとんどこればっかりでしょうから。

2. 現在すべてのコントロール(ボタンやエディットボックスなど)のカプセル化クラスとそのメンバ関数を実装していないが、WTLのWin32コントロールのカプセル化クラス名と同一クラス名にしてあるので、WTLのソースコードをそのままコピー&ペーストすれば(若干変更必要。後述)そのまま使える。
なので、WTLの開発者が新たに覚えることは非常に少ない。

    ver1.1からActiveXコントロールにも対応しました

3.WTL、ATLに依存していないので、コンパイラやMicrosoftの意向に左右されることない。
可搬性も高いと思われる


4.MFCより優れたデータ交換クラスを実装。この部分だけが唯一のオリジナルといえる所。MFCと同じ要領で使える。またMFCのデータ交換クラスと使い勝手が同じなのでMFCを使っている人にもわかりやすい。WTLはこのデータ交換部分は非常に貧弱で、WTLだけしか知らない人なら、Nwclのデータ交換クラスを見たら相当強力と思う。


この変数は、コントロールとそれに対応するデータ交換変数が完全に一対一の対応で、事実上普通の変数とみなしてコーディングすることが可能。ラジオボタンやチェックボックスのデータ交換変数なら、メッセージハンドラ上にこれらのコントロールに関連する初期化・終了処理をまったく記述する必要なし。MFCで言うところのUpdateData()に相当する関数をいちいち呼び出す必要なし。

ただし、独自データ交換マップを記述する必要がある(要するにごちゃごちゃした初期化処理、終了処理はその内部で記述されている)。


以下、Nwclのダイアログクラスの定義例


class CTestDialog :public CDialog{

public:

//データ交換変数
//定義方法 
//DDX_var<コントロールに対応したクラス名, リソースID,データ交換変数(オプション)> 変数名
  
DDX_var edit_ctrl;
DDX_var check_box;
DDX_var radio_button;
 

//データ交換マップ
BEGIN_DDX_MAP  
	//上で宣言した変数とウィンドウハンドルを関連付ける
	DDX_BIND_HWND(edit_ctrl)
	DDX_BIND_HWND(check_box)
	DDX_BIND_HWND(radio_button)
	//値の制限を設定
	DDX_SET_MINMAX(edit_ctrl,_T("エディットボックス"),0,200)
END_DDX_MAP

//メッセージマップ
//(要はこの中身がダイアログプロシージャなのだ)
BEGIN_DLG_MSG_MAP
	//ダイアログ初期化時のメッセージハンドラ定義
	MSG_INITDLG(OnInitDialog)
END_DLG_MSG_MAP

int OnInitDialog(){return 1;}//メッセージハンドラの実装部
};


このデータ交換変数はWindowsコントロールクラスの派生クラスオブジェクトでもあるので、
以下のこんな記述も同時に記述しても問題ありません。

edit_ctrl = 123;
edit_ctrl.SetWindowText(_T("123"));

この2つのコードは同じことをしています。この例からわかりますが、値を設定すればすぐにそれがコントロールに反映されるのでMFCのUpdateData関数に相当するものが必要ありません。
 また、ダイアログが表示していない時や、表示後もこのデータ交換変数は有効で、ユーザがウィンドウ表示前、表示後にコントロールから値を拾ったり反映させるコードをわざわざ記述する必要はなくコードの可読性があがります。

例えば
CTestDlg dlg;
int val=123;
dlg.edit_ctrl = val;
if( dlg.DoModal() == IDOK){
    val = dlg.edit_ctr;
}
とMFCのように記述しても問題ありません。ダイアログクラス内にある、独自メッセージマップとデータ交換マップが、コントロールの値の入出力に関する煩雑なコードを実装していますので、非常にすっきりとしたものになります。

注意:データ交換変数はテンプレートクラスオブジェクトなので、純粋に単純な型の変数(例えばint型の変数)のように振舞うことができません。指定した変数以外の代入以外の操作は、型を明示(キャスト)する必要があります。

-----------

このライブラリを使えばMicrosoftにもWTLにも翻弄されません。コード量が少なく、すぐに中身を理解できると思いますので簡単に変更できるでしょう。.libファイルがあれば他のC++コンパイラでも使用可能だと思います。まあ、Win32のコントロール自体が廃れていくから、現時点でもそして時間がたつごとにさらに意味のなくなっていくライブラリだと思いますが。

C++で開発してちょっとしたGUIが必要かなというときに重宝するライブラリでしょう。
でもMicrosoftはフリーのVisualStudio Expressではリソースエディタ(C/C++用)を同梱していません。まったく持って意地悪なものです。だから結局製品版を買うか、もしくはフリーのリソースエディタが必要になります。私はVisualStudio6.0(もう10年以上前の製品)のリソースエディタを使用していますが。

自分のライブラリで開発すると実感しますが、リソースIDの管理が面倒ですね。MFC Class Wizardの有り難みがわかります。同じものを作ろうかと思いましたが、そこまでプログラムに時間を割きなくないです。

現在のところダイアログボックスとプロパティシートのみの対応です。
その上に載る各種コントロールのカプセル化も自分が必要な部分しか実装していません。

しかし、WTLのソースコードを見ればわかりますが、コントロールのカプセル化クラスの実装部分は、単にSendMessage()関数をラップした程度のものばかりで、その中のメンバー関数を私が作ったNwclにあるWTLにあるクラスと同名のクラスに単純にコピー&ペーストして、ATLASSERTをassertに置換してしまえばほとんどが使えてしまいます。


MFCやWTLでプログラミングすれば大方察しがつくと思いますが、

「メッセージマップというのが要するにウィンドウプロシージャやダイアログプロシージャの中のswitch case文なんだろうな、だからその部分を作ってしまえばWTLで定義されているウィンドウコントロールをカプセル化したクラスも簡単に移植できるだろう」

と思って作ったのです。実際WTLのコードを見る限りそうでした。全部は見ていませんが。

生のWin32APIを使ってC言語などでWindowsプログラム開発をしている人には自明のことですが、Win32のGUIを実装するというのは、ウィンドウやダイアログがいろいろ出力するメッセージを拾って処理し、その処理の際、そのウィンドウに各種メッセージを出して制御するのが大まかな作業なのです。

メッセージを拾うところは、ウィンドウプロシージャとかダイアログプロシージャといわれる関数で、その関数をC言語で実装するなら、必ずswitch()〜case文を使って、各メッセージID(整数値)ごとに処理を割り振るのです。C++クラスライブラリはそのcase以降に仮想関数を設けるのです。 

ウィンドウプロシージャ例:


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{

	switch (message)//メッセージの種類
	 {
	case WM_COMMAND:
		switch(LOWORD(wParam)){
		case IDC_COPY:VirtualFunction();break;
		case IDC_CUT:VirtualFunction2();break;
		case IDC_DELETE:VirtualFunction3();break;
		}
	}

}
//以下省略


この部分を#define文で書いたものがメッセージマップなのです。そしてこのウインドウプロシージャやダイアログプロシージャをいかにC++のクラス内に取り込むというのが厄介なのです。ウインドウプロシージャはスタティック関数なので、C++のメンバ関数として使う場合、トリッキーなことをしなくてはなりません。



NWCLの使い方。


初期化

インクルード

#include "nwcl.h"

nwcl.hがライブラリのコードが入ったヘッダファイルです。

ソースコードに以下の文を必ず入れてください。1つだけ定義すればいいです。


extern "C"{
class NWCWinApp app_obj;
};

WinMain()関数などのスタートアップ関数直後にアプリケーションのインスタンスハンドルとアプリケーション名を入力してください。

nwcl::app_obj.set_instance(hInstance);
nwcl::app_obj.app_name = _T("アプリケーション名");

インスタンスハンドルの入力は必須です。

クラス定義

ダイアログボックス
class ダイアログクラス名 :public CDialog<ダイアログクラス名,リソースID>{
};

プロパティページ

class プロパティページクラス名 :public CPropertyPage<プロパティページクラス名,リソースID>{
};



各クラス名は、上位クラスであるテンプレートクラスに記述するクラス名と同一にしてください。
リソースIDは、ダイアログボックス、プロパティページのリソースIDを指定します。

データ交換変数の宣言フォーマット

DDX_var<コントロールに対応したクラス名,リソースID,データ交換変数の型(オプション)> 変数名;

コントロールに対応したクラス名はnwcl.hを見てください。

データ交換変数の型は、整数型をいくつか用意していますが、intにしておけば問題は出にくいはずです。他にベクターもどきで定義されているString(文字列)を指定可能です。

言うまでもないと思いますが、データ交換変数の型はリストビューなど複雑なコントロールには対応していません。


データ交換関数のマップ
BEGIN_DDX_MAP
DDX_BIND_HWND(データ交換変数)
//値の制限を設定
DDX_SET_MINMAX(データ交換変数,_T("コントロール名"),最小値,最大値)
END_DDX_MAP

データ交換変数をまったく定義していない場合でもクラス定義内に以下の定義は必須です。

BEGIN_DDX_MAP
END_DDX_MAP

理由は必ずこの定義内にある関数を呼び出すからです。
この定義はデータ交換変数とそれに対応するコントロールのウィンドウハンドルを関連付け、値の制限幅も指定します

DDX_SET_MINMAXマクロを定義する際、データ交換変数の型がint型でないとエラーが出る場合があります。


メッセージマップの定義

最低でも
BEGIN_DLG_MSG_MAP
END_DLG_MSG_MAP
をクラス内に定義(単に書き込むだけ)する必要があります。


この2つの定義内にメッセージハンドラを記述します。

BEGIN_DLG_MSG_MAP
    
	//WM_INITDIALOGメッセージに対応するメッセージハンドラ
	MSG_INITDLG(メッセージハンドラ)
    
	//WM_COMMANDメッセージ処理
	COMMAND_HANDLER_BEGIN
		//通知メッセージごとのコマンドハンドラ
		COMMAND_NOTIFY_HANDLER(リソースID,通知コード,メッセージハンドラ)
    
	//WM_COMMANDメッセージ処理終了
	COMMAND_HANDLER_END
	//コモンコントロール専用のメッセージハンドラ(WM_NOTIFYメッセージ処理)
	CMMNCTRL_NOTIFY_HANDLER_BEGIN
		//汎用
		CMMNCTRL_NOTIFY_ID_HANDLER(リソースID,通知コード ,メッセージハンドラ)
		//プロパティシートのOKや適用ボタン専用のメッセージハンドラ
		CMMNCTRL_NOTIFY_HANDLER(通知コード,メッセージハンドラ)
	CMMNCTRL_NOTIFY_HANDLER_END
END_DLG_MSG_MAP


これはダイアログプロシージャの中身そのものをマクロで隠蔽しただけのものです。
実際にコンパイルされるコードは以下のような感じになっています。

INT_PTR Procedure(UINT message,WPARAM wParam,LPARAM lParam ){ 

switch(message) {
	//MSG_INITDLG()
	case WM_INITDIALOG :return handler();
	
	//#define COMMAND_HANDLER_BEGIN
	case WM_COMMAND:{ 

		//#define COMMAND_NOTIFY_HANDLER(cmd_id,notify_code,handler) 
		if(LOWORD(wParam)==cmd_id && HIWORD(wParam)==notify_code){ handler(); return 0;} 
	}

	//#define CMMNCTRL_NOTIFY_HANDLER_BEGIN 
	case WM_NOTIFY:{LPNMHDR nmhdr = (NMHDR*)lParam; 

	//CMMNCTRL_NOTIFY_ID_HANDLER
	if(nmhdr->idFrom ==id && nmhdr->code==notification){ handler(lParam);return 0;} 

	}
}
}
注意:ver1.1からダイアログのOnOkハンドラは仮想関数になり、明示的に何も記述しなくてもよくなりました。
以前は明示的に記述しないとダイアログが消えない問題がありました
 
実際の使用方法はサンプルコードを参照してください。 

クラスライブラリのカスタマイズ方法  

コントロールクラスのカスタマイズ方法は、単純にWTLのコントロールクラス(WTLのインストール先のatlctrls.h内にある)のメンバ関数をコピー&ペーストして、ATLASSERT部分をassertに変更するだけでほとんどが使用可能になります。 ヘッダファイルnwcl.h には必要と思われる部分しかメンバ関数のコピーを行っていません。 WTLのメンバ関数使用が問題、もしくは嫌なら、昔からあるコントロールを制御するマクロをメンバ関数にしてもいいでしょう。どちらにせよ、コードに違いはほとんどありません。 

例:選択したリストビューの項目数を取得するマクロ: UINT ListView_GetSelectedCount( HWND hwnd); を、メンバ関数にする

(class CListViewCtrl内で記述する

UINT GetSelectedCount(){ return ListView_GetSelectedCount(m_hWnd);} 

 

要は、マクロの接頭語に相当するコントロール名を削除したものをメンバ関数にして、引数はマクロのウィンドウハンドルを削ったものにすればいいのです。上の例だと引数はゼロになります。そして定義部分で、上位クラスのCWindowにあるm_hWndを引数にしてマクロを呼び出せばいいのです。 各コントロールのマクロの一覧は検索サイトで "msdn Control Library"と検索して、たぶん最初に出てくるmsdn(Microsoft Developer Network)内のControl Libraryを見れば、全コントロールのメッセージIDと通知メッセージ、マクロを見ることができます。

これ以外に独自にメンバ関数を定義することももちろん可能です。これを自由に行うことができれば本当にWTLやMicrosoftの意向に左右されません。Nwclには、私が作ったいくつかのメンバ関数があります。演算子のオーバーロードを使ったものが見つかるはずです。長ったらしい名前のメンバ関数を使うより、演算子を使ったほうが見やすくなります。私が作った中で一番コードが長い独自メンバ関数は、ラジオボタンのデータ交換変数がらみのコードです。これはMFCと使い勝手を同じにするために少々長くなってしまいました。

ActiveXコントロール利用のための実装方法

Ver1.1からActiveXコントロールを簡単にダイアログなどで使用することができます。これはEternal WindowsというサイトのFlashコントロールを使ったサンプルコードを改変して実現したものです。(サイト管理者に許諾は取ってあります)。この場を借りてEternal Windowsの管理者に御礼申し上げます。

ActiveXコントロールを使用する場合は、nwclcom.hをnwcl.hの後でインクルードしてください。

ActiveXコントロールクラスを使用するのに最低限必要なのは、ActiveXコントロールを実装しているCLSID(クラスID)で、それを操作するためには、それにアクセスするためのインターフェイスIDも必要になります。イベント(正確にはイベントシンク)も実装できます。これはWin32の用語で例えるならコールバックもしくはウィンドウプロシージャのようなものです。要するにこれらのIDを見つけてしまえば後は簡単に実装できるのです。

1.リソースエディタにダイアログに表示する枠を決める。

1.1CLSIDの取得

Express版でないVisualC++に付属のリソースエディタでは「ActiveXコントロールの挿入」と言うメニューがありますが、nwclではこれで挿入したコントロールを最終的には利用してはなりません。「最終的には」と言う意味は、いったん設定して、「ある」情報を取り出してから、消して欲しいと言うことです。それはCLSIDの事です。このメニューでリソースファイル(.rcファイル)にはCLSIDが記述されますので、テキストエディタから.rcファイルを読み込んでCLSIDを取り出します。”{}”で囲まれた英数字とハイフンが混じった文字列がCLSIDです。この方法だと楽にCLSIDを調べることができます。(VisualBasicやVisualC#のリソースファイルでも同様にクラスIDが記述されているかもしれません。)

VisualStudioが使えない場合はSDKなどに付属のOLE/COM Object Viewを使用して探す必要があります。詳しくはnwclcom.hの最初のコメント部分に書かれてありますので参照してください。

ちなみにWebBrowserコントロールのCLSIDは{8856F961-340A-11D0-A96B-00C04FD705A2}となります。

1.2表示領域を決める

CLSIDを取得できたら、リソースエディタで挿入したActiveXコントロールを削除します。その代わりにテキストコントロールを挿入します。表示領域を変えてください。テキストコントロールのリソースIDは通常ID_STATICになりますが、一意な名前になるよう変更してください。サンプルコードではIDC_STATIC_IEとしました。

2クラス宣言と、ActiveXコントロールのインスタンス作成方法

ActiveXコントロールクラスの宣言は以下の通り

CActiveXCtrl< /*イベントシンクのクラス名(オプション)*/ > *axctrl;

必ずポインタ変数で宣言してください。サンプルコードでは、CIPtr<>というインターフェイスポインタを適切に解放するテンプレートクラスで囲ってあってポインタ変数のように見えないのですが、CIPtr<>クラス内部でポインタ変数として宣言しています。

イベントクラスを入れないのであれば、テンプレートクラス名を記述する<>内に、何も書く必要はありません。デフォルトクラスが適用されます。

そして、このクラスオブジェクトはnew演算子で必ずインスタンスを作成する必要があります。(これはComponent Object Modelの仕様)

具体的には以下のようになります。


CActiveXCtrl< > *iptr;
iptr = new CActiveXCtrl<>( 
	/*Microsoft Web Browser CLSID*/
	L"{8856F961-340A-11D0-A96B-00C04FD705A2}",
	//リソースID、IDC_STATIC_IEの示す領域に表示
	GetDlgItem(IDC_STATIC_IE) //オーバーラップウィンドウのハンドルも指定可。その際CActiveXCtrl<>::OnSize(x,y)で表示領域の変更可
	);
if(iptr){
	//iptrを操作するためのインターフェイスポインタの取得
}
//最後に必ずRelease()を実行。(これはこのコントロールを乗せているウィンドウの終了処理時に行う)
iptr->Release();
3インターフェイスポインタの宣言と使用 

実際に操作するにはインターフェイスIDも必要になります。このIDはサンプルの例だとIWebBrowser2インターフェイスが使用できます。ActiveXコントロールごとに実装されているインターフェスは違いますが、IDispatchインターフェイスは必ず実装されていますので、このアドレスを取り出せば一応操作はできます。 ただ、コードが直感的ではありません。すべてinvokeと言うメソッド名で操作します。


   IWebBrowser2 *web;
   //QueryInterface「2」はActiveX特定のインターフェイスポインタを取り出すときに使用
   iptr->QueryInterface2(IID_PPV_ARGS(&web));//WebBrowser コントロールからIWebBrowser2インターフェイスを取得     
	
   IDispatch *web2;

   //IDispatchインターフェイスポインタを取得
   iptr->QueryInterface2(IID_PPV_ARGS(&web2));
クラスID(CLSID)が、どんなインターフェイスを持って、どんな操作(メソッド)を行えるかはOLE/COM Object Viewで探せばわかります。
ちなみにWeb表示は
         
 	VARIANT v; 
	v.bstrVal = SysAllocString(L"http://natsuhaze.jp/software/"); 
	v.vt = VT_BSTR; 
	web->Navigate2(&v,NULL,NULL,NULL,NULL); 
	SysFreeString(v.bstrVal); 

となります。

4.イベントシンクの実装

これはnwclcom.hに定義済みのCActiveXEventクラスの派生クラスを用意することで容易に実装できます。最低限必要なのは、実装しているイベントシンクを示すID(UUID)を使用して派生クラスを作成し、その派生クラス内でInvoke()の再定義することです。IDの探索方法はnwclcom.hの上部コメントに書いてあるので参照してください。実際の例は、サンプルプロジェクトを参照してください。

 


最後に

このクラスライブラリはWin32の初期のC言語の開発手法を知っていれば、内部構造を容易に理解できるはずです。また、これを使って開発する際は、各コントロールのメッセージを知っていることが鍵となります。それはWTLのWindowsコントロールクラスを見れば明らかです。

しかしWTLのソースコードだけを見ては通知メッセージがわかりません。言うまでもなく、通知メッセージとは、ボタンが押されたとか、フォーカスが変化したとかいうWindowsが発するメッセージのことです。これらのメッセージを処理することも必要なので、把握しておくことも必要です。
 結局それらもmsdnのホームページ内のControl Library以下に記載されていますので参照してください。
英語がわからない人で製品版のVisualStudioを持っている人なら、MFCのWindowsコントロールをカプセル化したクラスのソースコードを見て通知コードや各種メッセージの使い方を知るとよいでしょう。