タブストリップ型ダイアログを作る

 最近のアプリケーションの力の入れどころの1つに「カスタマイズ」があります。ユーザーの要求が多様になりわがままになってくると、それに応えるために様々な設定を用意し、その結果ダイアログがかなり増えることになります。

 そういうときに非常に有効なのが、タブストリップを使ったダイアログです。

<「まどかぶ」の設定用ダイアログ>

 では、こういった形のダイアログを作成する方法を見ていきましょう。


 あんまりもったいぶるのもなんなので、まず最初に種明かしをしておきます。タブストリップは使いません。使うのはプロパティシートとプロパティページです
 ダイアログを作成するときに、これみよがしにタブストリップコントロールがありますが、これは使いません。惑わされないように。

 では、いちばん簡単なタブストリップ型ダイアログを作成しましょう(難しいことは「プログラミングMFC:百科」−「プロパティシート」に載っています)。
 まず、1ページ目を作成してみます。ダイアログリソースを新しく作ってください。このダイアログが、タブストリップを押して開くページのひとつになります。
 次にこのダイアログをダブルクリックしてプロパティを表示させます。で、まずキャプションを変更します。このダイアログのタイトルですね。このキャプションが、タブストリップのタブに書かれます。たいがい、ダイアログのタイトルなんてどうでもいいんですけど、タブストリップになるときにはこれを頼りにしなければなりませんから、よく考えて解りやすく短い名前を付けましょう。ここでは簡単に「いちぺぇじめ」としておきます。
 次に、「スタイル」タブをクリックして、「スタイル」を「チャイルド」に、「境界線」を「細枠」に、「タイトルバー」を「オン」にします。

(注:上に書いたオンラインヘルプには「境界線」を「なし」にすると書いてありますが、それと「タイトルバー」を「オン」にすることは同時にできません。プロパティページを作る時に変更すればいいのかもしれませんが、ここでは触れません)

 最後に「その他のスタイル」タブをクリックして、「無効」を「オン」にしておきます。これで、準備が完了しました。
 ただ、これだけではちょっと寂しいので、エディットコントロールを1つ真ん中にでも置いておいてください。IDは「IDC_T1_EDIT」にでもしておいてください。

 次に、このダイアログのクラスを作成します。ダイアログを選択している状態で、クラスウィザードを開きます。そうすると、新規にクラスを作成する段階になるでしょう。
 クラスの名前は「CTab1」にでもしておいてください。そして、いちばん大事なことは基底クラスをCPropatyPageにしておくということです。CDialogクラスではダメです。
 クラスができたところで、エディットコントロールにメンバ変数を割り当てておきましょう。メンバ変数は「m_cEditStr」、クラスはCStringにしておいてください。

 さて、とりあえずタブストリップですから、ページは複数あった方がいいでしょう。今回は簡単に実装するだけですので、2ページだけ作成します。
 1ページ目と同じように、ダイアログを新規作成し、2ページ目のキャプションを「にぺぇじめ」とし、「スタイル」タブを開いて「スタイル」を「チャイルド」に、「境界線」を「細枠」に、「タイトルバー」を「オン」に、最後に「その他のスタイル」タブを開いて、「無効」を「オン」にしておいてください。そして、今回はリストボックスを張り付けておいてください。IDは「IDC_T2_LIST」としておいてください。
 次にクラスを作成します。クラスウィザードを開き、新規ダイアログのクラス名は「CTab2」、基底クラスはCPropatyPageにしておきます。ついでにリストボックスのメンバ変数も割り当てておきましょう。変数名は「m_cListBox」とし、カテゴリはコントロール、クラスはCListBoxクラスにしてください。

 さて、ここまでで下準備は終わりました。実際にダイアログを作成してみましょう。
 まず、ダイアログを表示するためのコマンドを作っておきましょう。メニューにでも適当にコマンドを作成し、そこにメンバ関数を割り当ててください。具体的にはメニューにIDが「ID_DLG_OPEN」のコマンドを作成し、ビュークラスを開いて、オブジェクトIDに「ID_DLG_OPEN」を選び、メッセージを「command」を選んで新規にハンドラ関数を作成します。この中にダイアログ表示のコードを書き込めば、アプリケーションを開いてそのメニューを選択したときにダイアログが開くという仕組みになるわけです。

 さて、実際にダイアログを作成してみましょう。まず、このビュークラスの上の方にあるインクルードファイルの一覧の中に「#include "Tab1.h"」「#include "Tab2.h"」を加えてください。
 次に、先ほど作成したメンバ関数(おそらくvoid CKABView::OnDlgOpen()となっているでしょう)の中に、次のようなコーディングをしてください。


void CKABView::OnDlgOpen()
{
	CTab1 cTab1;	//タブ1のオブジェクトを作成します。
	CTab2 cTab2;	//タブ2のオブジェクトを作成します。

	// ダイアログそのものを作成します。
	CPropertySheet cPropSht( _T( "タブストリップ型ダイアログ" ) );
	cPropSht.AddPage( &cTab1 );	//タブ1をダイアログに加えます。
	cPropSht.AddPage( &cTab2 );	//タブ2をダイアログに加えます。

	if( cPropSht.DoModal() )	//ダイアログを表示します。
	{
		// ここに「OK」ボタンが押されたときの反応を書き込みます。
	}	
}
	

 この時点でとりあえずビルドしてみましょう。うまく行けば、先ほど作ったメニューのコマンドを選択すると、「いちぺぇじめ」「にぺぇじめ」と書かれたタブが並ぶダイアログが表示されるでしょう。
 では、さらに細かく見ていきましょう。

 まず、2つのプロパティページのオブジェクトを最初に作成します。次に、プロパティシートのオブジェクトも作成します。ここでは、同時にダイアログのタイトル(タブストリップ型ダイアログ)を設定しておきます。
 CPropertySheetクラスは、まるでコモンダイアログの一種のように振る舞います。クラスの階層としては全く関係のない位置にあるCPropertySheetクラスですが、作成してDoModalメンバ関数を呼ぶだけでダイアログが作成されます。
 実際には、ダイアログを作る前にページを割り当てておきます。CPropertySheetクラスAddPageメンバ関数を使って、プロパティページのひとつひとつを加えていきます。ページは加えていく順番に並ぶことを忘れないでください。
 そして、最後にDoModalメンバ関数を呼び出してダイアログを表示します。そして、どのボタンが押されて終了したかを戻り値として受けて、その結果次第で設定の内容を書き込むか書き込まないか決めるわけです。この辺は、普通にダイアログを作るときと同じですね。

 ここで注意をひとつ。CPropertySheetクラスは、実際コモンダイアログに非常に近い存在です。そのため、ダイアログの形状を変更したりするのは結構大変です。最初の例のように、デフォルトで「更新」ボタンが付いていたりします。この辺の処理は前述のオンラインヘルプに載っている(なんか名称が変わってる。訳の関係?)ので、そちらを参考にしてください。

 さて、最後にメンバ変数への代入の問題点を見てみましょう。最初のCTab1のエディットボックスに文字を格納し、また、その文字を受けるには次のようにします。


void CKABView::OnDlgOpen()
{
	CTab1 cTab1;	//タブ1のオブジェクトを作成します。
	cTab1.m_cEditStr = _T( "あうあう!" );	//エディットボックスに文字を表示します。

	CTab2 cTab2;	//タブ2のオブジェクトを作成します。

	// ダイアログそのものを作成します。
	CPropertySheet cPropSht( _T( "タブストリップ型ダイアログ" ) );
	cPropSht.AddPage( &cTab1 );	//タブ1をダイアログに加えます。
	cPropSht.AddPage( &cTab2 );	//タブ2をダイアログに加えます。
	
	if( cPropSht.DoModal() )	//ダイアログを表示します。
	{
		//「OK」が押されたなら、メンバ変数の内容を表示します。
		MessageBox( cTab1.m_strEdit, _T( "エディットボックスの中身" ) );
	}	
}
	

 まずタブ1からです。このエディットコントロールへの文字列の格納の方法は、普通にダイアログを使用するときと同じですね。これは、CPropatyPageクラスが、通常のダイアログとほぼ同じように振る舞うことを示しています。CPropertySheetクラスは単なるまとめ役でしかありません。具体的な操作はここのプロパティページへと直接アクセスします。

 さて、次にタブ2のリストボックスへと文字列を格納してみましょう。


//〜〜略〜〜

	CTab2 cTab2;	//タブ2のオブジェクトを作成します。
	cTab2.m_cListBox.AddString( _T( "テスト" ) );	//エラーが出ます。

//〜〜略〜〜

	if( cPropSht.DoModal() )	//ダイアログを表示します。
	{
		//「OK」が押されたなら、メンバ変数の内容を表示します。
		MessageBox( cTab1.m_strEdit, _T( "エディットボックスの中身" ) );
		CString str;
		cTab2.m_cListBox.GetText( 0, str );
		MessageBox( str, _T( "一番目の文字列" ) );
	}
}
	

 さて、これを実際にビルドして実行すると、ASSERTに引っかかってプログラムが終了します。つまり、このプログラムにはバグがあるわけです。
 その原因は、リストボックスがまだ構築されていないうちに操作をしようとしたからです。

 確かにm_cListBoxメンバ変数は、ただの変数のように見えますが、れっきとしたコントロールです。そのため、実際にリストボックスが表示されていないこの段階では、リストボックスに項目を加えることはできないのです。

 では、いつするのでしょうか? ダイアログが作成されるときにはOnInitDialogメンバ関数が呼び出されます。ダイアログが表示されるときにはWM_INITDIALOGメッセージが呼び出されるからです。
 そこで、まず今のリストボックスに加えた部分を削除します。次にTab2.cppを開き、オブジェクトのIDはそのまま、メッセージにWM_INITDIALOGを選んで新規にハンドラを追加します。そして、次のようにコーディングします。


BOOL CTab2::OnInitDialog() 
{
	CPropertyPage::OnInitDialog();

	m_cListBox.AddString( _T( "テスト" ) );	//リストボックスに加えます。

	return TRUE;  // コントロールにフォーカスを設定しないとき、戻り値は TRUE となります
	              // 例外: OCX プロパティ ページの戻り値は FALSE となります
}
	

 m_cListBoxメンバ変数CTab2のメンバなので、最初にCTab2と書く必要はありません。

 さて、これでアサートもしないし大丈夫……と思ったらまだ甘いです。何度か使ってるうちに不具合が出るかもしれませんね。なぜでしょうか?

 さて最後になりました。ここで問題なのは、WM_INITDIALOGメッセージの呼ばれるタイミングです。このメッセージは、プロパティページの場合にはそのページが表示されたときに呼び出されます。リストボックスに項目を加えたときにそれは解りましたね? それがなぜ問題なのでしょうか。
 先ほど、cPropSht.DoModalメンバ関数を呼び出して、OKが表示されたときにcTab2.m_cListBoxにアクセスしました。でも、もしタブ2がめくられずに「OK」ボタンが押されたとしたらどうなるでしょうか。 リストボックスにはなんの値も設定されていないのです。なぜなら、そのタブがめくられた時に初めて、WM_INITDIALOGが呼び出されるからです

 この問題は特にエディットコントロールに整数値を設定するときに大きな問題になるようです。エディットコントロールのメンバ変数に整数値を設定する前にデータを得ようとすると、とんでもない値が返ってきてしまいます。

 この解決法は、リストボックスとは別に変数を作っておき、アクセスはそこにすることで解決します。例えば、CTab1クラスCStringArrayクラスm_cListStrAryメンバ変数を作ってしまいます。そして、データのアクセスはそのメンバ変数に任せてしまい、データが変更された時にだけリストボックスの中身をそのメンバ変数を元に作成するというものです。実際にしてみると……


BOOL CTab2::OnInitDialog() 
{
	CPropertyPage::OnInitDialog();

	// コンストラクタかCTab2を構築した直後に、m_cListStrAryに値を加えておきます。
	ListUpDate();
	// リストボックスを更新するための関数を別に作り、その関数の中で
	// m_cListStrAryを元にリストボックスの中身を埋めるようにします。

	return TRUE;  // コントロールにフォーカスを設定しないとき、戻り値は TRUE となります
	              // 例外: OCX プロパティ ページの戻り値は FALSE となります
}

void CTab2::ListUpDate()
{
	m_cListBox.ResetContent();	//リストボックスの中身をクリアします。
	for( int i = 0; i <= m_cListStrAry.GetSize() - 1; i++ )
	{
		//リストボックスの中身を埋めていきます。
		m_cListBox.InsertString( i, m_cListStrAry.GetAt( i ) );
	}
}
	

 と、このような感じです(配列の中身を埋める部分は省略しました)。

 以上でタブストリップ型ダイアログの簡単な作成の仕方と、その注意点を書いてみました。その他にも、CPropertySheetクラスにはウィザード方式のダイアログを作成することもできますし、更新ボタンやその他の機能も与えられているので、結構面白い使い方ができると思います。


 さて、最後の最後におまけです。
 最初の例にあげたダイアログには、タブストリップがスクロールできるようになっています。それとは別に、「秀丸エディタ」のような縦に列を作っていくタブストリップもあります。
 そして、CPropertySheetは後者の方しかありません。スクロールさせるタイプのタブストリップを作成できないのです。CPropertySheetクラスには、タブストリップコントロールを取得するためのGetTabControlメンバ関数がありますが、実はCTabCtrlクラスにも、タブストリップの状態を変更するメンバ関数はないのです。

 この問題を解決する方法は、KnowledgeBaseKB: Visual C++ How to Create a Property Sheet That Has Scrolling Tabs in MFCにありました。それは、まずCPropertySheetクラスを基底クラスとした新しいクラスを作成し、プロパティシートのクラスをその新しく作ったクラスで構築し、そしてその新しいクラスのOnCreateメンバ関数の最初に次のように記述するというものです


	EnableStackedTabs(FALSE);
	

 EnableStackedTabsメンバ関数なるものの意味、はっきりいって解りません。おそらく表には出ていないCPropertySheetクラスのメンバなのでしょう。何れにせよ、この方法で解決します。
(C)KAB-studio 1997 ALL RIGHTS RESERVED.