最終更新日時:

WindowsのMFCに入門してみる

 ちょっと忙しくてブログを投稿する気が起きなかったが、それもいかんなと思い、少し前に調べたMFCの調査メモを適当に整理したものを投稿する。

はじめに

 Windowsでデスクトップアプリを作成するとしたら、MFCやWindows Form、WPF、UWPなどが挙げられたりするが、訳あって今回はMFCにを触ってみる。
 なお、今回は入門的な位置づけだが、主にダイアログアプリケーションの作成の本当に基本的な部分のみを触る。そのため、以下のような高度(?)な機能などは触らない。

  • 分割ウィンドウ

  • プロパティシート

  • (リソースファイルを使わない)ダイアログの動的生成

  • 画面遷移

 動作はWindows 10におけるVisual Studio 2022で確認している。また、今回作成分のコードは以下にzipで圧縮したものを置いてある。

環境構築

 まずは、MFCのプロジェクトを立ち上げることができるようにするための具体的手順を示す。

  • Visual Studio Installerを起動

  • Visual Studio 2022の変更に関する項目を選択

  • 「C++によるデスクトップ開発」を選択

  • 右のチェックリストにあるC++ MFCに関する項目にチェックを入れる

    インストール対象の選択
  • インストールを実施

 これで、MFCのプロジェクトが作成可能になる。次に、MFCによるダイアログアプリケーションのプロジェクトを立ち上げる手順を示す。

  • MFCで検索をかけることにより「MFC アプリ」という項目が見つかるため、それを選択

    プロジェクトの選択画面
  • 他のプロジェクトと同じようにプロジェクトの設定を行う

  • プロジェクトの設定を行う過程でMFCアプリケーションの構成を尋ねられるため、「アプリケーションの種類」をダイアログベースを選択し、「完了」ボタンを押下

    MFCアプリケーションの設定

 これでプロジェクトの作成が完了する。後は、F5でも押してビルドをすると、初期状態のダイアログとボタンが表示されるだけのプログラムが起動する。

MFCのデフォルトのプログラムの実行結果

 なお、今回はMFCTestという名前のプロジェクトを作成した。

簡単なGUIとイベントの設置

 基本的には、以下の画像の左にあるようなツールボックス(ツールボックスの配置はVisual Studioの利用者の環境依存)からドラッグ&ドロップによりGUIを直接設置していくことができる。

ダイアログのエディットの様子

ボタン

 まずは、ボタンを設置してみる。ボタンに表示されるテキストについては「プロパティ」の「キャプション」(下画像の左のリストのハイライトされている項目)から設定をすることができる。

ボタンのプロパティの設定の様子

 そして、設置したボタンをダブルクリックすると、C++のソースにジャンプし、ボタンクリックについてのイベントハンドラが生成される。そのイベントとして、メッセージボックスを設置してみる。

void CMFCTestDlg::OnBnClickedButton1()
{
	while (IDNO == AfxMessageBox(_T("ボタン1を押下"), MB_YESNO))
	{
		// NOが選択されたらもう一度表示させる
	}
}
C++

 これを実行して「ボタン」ボタンを押下すると以下のようなメッセージボックスが表示され、「はい」を押さない限りずっと表示される。

メッセージボックスの表示の様子

チェックボックス

 チェックボックスについてもボタンと同じ要領で設置をし、イベントを定義することができるため、適当に機能を作ってみる。機能の概要についてはコメントの通りである。

void CMFCTestDlg::OnBnClickedCheck1()
{
	// 隣接するチェックボックスを反転する
	CButton* pCheck2 = (CButton*)GetDlgItem(IDC_CHECK2);
	pCheck2->SetCheck(pCheck2->GetCheck() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED);
}


void CMFCTestDlg::OnBnClickedCheck2()
{
	// 隣接するチェックボックスを反転する
	CButton* pCheck1 = (CButton*)GetDlgItem(IDC_CHECK1);
	CButton* pCheck3 = (CButton*)GetDlgItem(IDC_CHECK3);
	pCheck1->SetCheck(pCheck1->GetCheck() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED);
	pCheck3->SetCheck(pCheck3->GetCheck() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED);
}


void CMFCTestDlg::OnBnClickedCheck3()
{
	// 隣接するチェックボックスを反転する
	CButton* pCheck2 = (CButton*)GetDlgItem(IDC_CHECK2);
	pCheck2->SetCheck(pCheck2->GetCheck() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED);
}
C++

 これを実行して適当なチェックボックスにチェックを入れると、隣接するチェックボックスの中身が反転する。

チェックボックスの操作の様子

ラジオボタン

 ラジオボタンの設置についてはラジオボタンの生成順序が重要である。まず、以下のような2つのラジオボタンのグループを生成することを考える。「Radio1」のような各文字列はラジオボタンのIDである。

  • 「Radio1」、「Radio2」、「Radio3」

  • 「Radio4」、「Radio5」、「Radio6」

 そして、「Radio1」から連番で「Radio6」までのラジオボタンを順番に生成する。そして、「Radio1」と「Radio6」について、「プロパティ」の「グループ」をTrueに設定する。

ラジオボタンのプロパティの設定の様子

 これにより2つのラジオボタンのグループが生成される。

 これでラジオボタンの操作ができるようになる。プログラム上からラジオボタンを操作するサンプルとして、チェックボックスをクリックすることで、ラジオボタンのチェックを移動させるという処理を追加してみる。

/*
* 指定したダイアログ上のラジオボタンのグループのチェックを次の要素に移動する
* @param diag ダイアログのハンドラ
* @param radioGroup ラジオボタンのリスト
*/
template <int N>
static void SetNextCheck(const CDialog& diag, const std::array<int, N>& radioGroup) {

	for (int i = 0; i < N; ++i) {
		CButton* radio = (CButton*)diag.GetDlgItem(radioGroup[i]);
		// チェックされていれば次の要素にチェックを移動させる
		if (radio->GetCheck() == BST_CHECKED) {
			radio->SetCheck(BST_UNCHECKED);
			((CButton*)diag.GetDlgItem(radioGroup[(i + 1) % N]))->SetCheck(BST_CHECKED);
			break;
		}
	}
}

void CMFCTestDlg::OnBnClickedCheck3()
{
	// 隣接するチェックボックスを反転する
	CButton* pCheck2 = (CButton*)GetDlgItem(IDC_CHECK2);
	pCheck2->SetCheck(pCheck2->GetCheck() == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED);

	int radioGroup1[] = { IDC_RADIO1, IDC_RADIO2, IDC_RADIO3 };
	int radioGroup2[] = { IDC_RADIO4, IDC_RADIO5, IDC_RADIO6 };
	// ラジオボタンのチェックを変える
	SetNextCheck(*this, std::array<int, 3>{ IDC_RADIO1, IDC_RADIO2, IDC_RADIO3 });
	SetNextCheck(*this, std::array<int, 3>{ IDC_RADIO4, IDC_RADIO5, IDC_RADIO6 });
	// 選択されたやつ自体は以下で取得できる
	//GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3);
	//GetCheckedRadioButton(IDC_RADIO4, IDC_RADIO6);
}
C++

 これを実行して適当なラジオボタンを選択すると、ラジオボタンのグループが2つ存在することが確認できる。また、「Check3」のチェックボックスをクリックすることにより、ラジオボタンの選択要素が次に移動する事が確認できる。

ラジオボタンの操作の様子

 MFCは単なるWindows APIのラッパーであるため、配置をしたダイアログの要素についてはリソースエディタから編集をすることができる。リソースエディタについてはソリューションエクスプローラの「リソースファイル」を展開し、リソースファイルを右クリックして「コードの表示」をクリックすることにより確認をすることができる。

リソースファイルのコードの表示の手順

 その中の現段階におけるダイアログの構成は以下の部分である。これを見ればわかる通り、要素を定義した順番に列挙されていることが分かる。

IDD_MFCTEST_DIALOG DIALOGEX 0, 0, 320, 200
STYLE DS_SETFONT | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 9, "MS UI Gothic", 0, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,209,179,50,14
    PUSHBUTTON      "キャンセル",IDCANCEL,263,179,50,14
    CTEXT           "TODO: ダイアログのコントロールをここに配置",IDC_STATIC,10,96,300,8
    PUSHBUTTON      "ボタン",IDC_BUTTON1,153,179,50,14
    CONTROL         "Check1",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,264,128,38,10
    CONTROL         "Check2",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,264,144,38,10
    CONTROL         "Check3",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,264,160,38,10
    CONTROL         "Radio1",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON | WS_GROUP,154,129,36,10
    CONTROL         "Radio2",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,154,144,36,10
    CONTROL         "Radio3",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,154,160,36,10
    CONTROL         "Radio4",IDC_RADIO4,"Button",BS_AUTORADIOBUTTON | WS_GROUP,204,128,36,10
    CONTROL         "Radio5",IDC_RADIO5,"Button",BS_AUTORADIOBUTTON,204,144,36,10
    CONTROL         "Radio6",IDC_RADIO6,"Button",BS_AUTORADIOBUTTON,204,160,36,10
END
Plain text

 もちろん、これを直接触ることにより調整も可能である。ここで着目するのは、IDC_RADIO1からIDC_RADIO6の部分である。この要素が作成したラジオボタンのハンドラであり、IDC_RADIO1IDC_RADIO4についてはWS_GROUPというフラグがついている。Windows APIにおけるグループとはWS_GROUPが指定された要素をから次のWS_GROUPが指定された要素(存在しないならば終端要素)までを1つのグループとして扱うため、定義の順番が重要であるということである。

エディットコントロール・スタティックテキスト

 エディットコントロールとはいわゆるテキストボックスであり、スタティックテキストはただのテキストを表示するものである。エディットコントロールに関連して、リッチエディット2.0コントロール(以後単にリッチエディットコントロールとする)というものががあるが、これはエディットコントロールをリッチにしたものであり、書式の利用ができる。これら3つの要素を設置してみる。作成内容としては以下の内容とする。

  • エディットコントロールとリッチエディットコントロールの変更を検知したとき、入力したテキストをスタティックテキストへ反映させる

  • リッチエディットコントロールでは <>で囲まれた文字列をURLと判断してハイパーリンクを設置する

 エディットコントロールとリッチエディットコントロールには「複数行」と「戻り値が必要」、「自動 VScroll」、「水平スクロール」「垂直スクロール」をTrueに設定する。注意点としては 「戻り値が必要」をTrueにすることが改行を入力するための指定である
 また、 テキストの変更を要するスタティックテキストにはIDを自分で付ける必要がある(今回はIDC_STATIC2とした)。
 というわけで、実際に実装をしてみる。まず、リッチエディットコントロールの利用には共有ライブラリの読み込みが必要であるため、C[プロジェクト名]App::InitInstance()で共有ライブラリの読み込みと開放を行う。今回作成したプロジェクトの名前はMFCTestであるためCMFCTestApp::InitInstance()で行えばいい。

BOOL CMFCTestApp::InitInstance()
{
	// リッチエディット2.0を利用するためのDLLの読み込み
	HMODULE hLibRichEdit = LoadLibrary(_T("RICHED20.DLL"));

	// 略

	if (hLibRichEdit) {
		FreeLibrary(hLibRichEdit);
	}

	return FALSE;
}
C++

 また、リッチエディットコントロールは初期状態ではイベントを発火してくれないため、イベントマスクを設定する必要がある。そのため、C[プロジェクト名]Dlg::OnInitDialog()でイベントマスクの設定を行う。今回作成したプロジェクトの名前はMFCTestであるためCMFCTestDlg::OnInitDialog()で行えばいい(メインのウィンドウのGUIの初期化の記述はCMFCTestDlg::OnInitDialog()で記述する必要がある)。

BOOL CMFCTestDlg::OnInitDialog()
{
	// 略

	// リッチエディットの変更イベントとリンククリックイベントを取得できるようにする
	CRichEditCtrl* richEdit = (CRichEditCtrl*)GetDlgItem(IDC_RICHEDIT21);
	richEdit->SetEventMask(richEdit->GetEventMask() | ENM_CHANGE | ENM_LINK);

	return TRUE;
}
C++

 次にイベントを設置していく。まずは、エディットコントロールとリッチエディットコントロールの変更内容をスタティックテキストへ反映し、リッチエディットコントロールでハイパーリンクを生成するイベントを設置する。イベントの設置方法はこれまでと同様にして、設置したGUIをダブルクリックするだけである。 イベントの実施内容のプログラムについては以下の通りである。特に、FindText()のフラグの指定には要注意である。

void CMFCTestDlg::OnEnChangeEdit1()
{
	// IDC_EDIT1の入力をそのままIDC_STATIC2に表示する
	CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT1);
	CStatic* st = (CStatic*)GetDlgItem(IDC_STATIC2);
	CString str;
	edit->GetWindowText(str);
	st->SetWindowText(str);
}


void CMFCTestDlg::OnEnChangeRichedit21()
{
	// IDC_RICHEDIT21の入力をそのままIDC_STATIC2に表示する
	CRichEditCtrl* richEdit = (CRichEditCtrl*)GetDlgItem(IDC_RICHEDIT21);
	CStatic* st = (CStatic*)GetDlgItem(IDC_STATIC2);
	CString str;
	richEdit->GetWindowText(str);
	// 以下でもOK
	//richEdit->GetTextRange(0, richEdit->GetTextLength(), str);
	richEdit->GetWindowText(str);
	st->SetWindowText(str);

	// カーソル情報をバッファする
	CHARRANGE cr;
	richEdit->GetSel(cr);

	// 雑に<~>で囲まれた範囲をURLとしてハイパーリンクを設置する
	FINDTEXTEX ft;
	ft.chrg.cpMin = 0;
	ft.chrg.cpMax = richEdit->GetTextLength();
	while (true) {
		int first = -1;
		int last = -1;

		// <の探索
		ft.lpstrText = _T("<");
		if (richEdit->FindText(FR_MATCHCASE | FR_DOWN, &ft) == -1) break;
		first = ft.chrgText.cpMax;
		
		// 次の探索開始位置のセット
		ft.chrg.cpMin = ft.chrgText.cpMax;

		// >の探索
		ft.lpstrText = _T(">");
		if (richEdit->FindText(FR_MATCHCASE | FR_DOWN, &ft) == -1) break;
		last = ft.chrgText.cpMin;

		// [first, last)の文字列をハイパーリンクとして扱う
		CHARFORMAT2 cf;
		richEdit->SetSel(first, last);
		richEdit->GetSelectionCharFormat(cf);
		cf.dwMask |= CFM_LINK;
		cf.dwEffects |= CFM_LINK;
		richEdit->SetSelectionCharFormat(cf);

		// 次の探索開始位置のセット
		ft.chrg.cpMin = ft.chrgText.cpMax;
	}

	// カーソル情報を復元する
	richEdit->SetSel(cr);
}
C++

 次に、リッチエディットコントロールでのハイパーリンククリック時のイベントを定義する。エディタ上で対象となるリッチエディットコントロールを右クリックすると、「イベントハンドラーの追加」というものがあるため、これを選択する。

イベントハンドラの追加の様子1

 これによりダイアログが出現し、以下の設定をして「OK」を押下するとイベントが定義される。このイベントの定義方法はダブルクリックによりイベントを生成するよりも一般的な定義方法であり、ダブルクリックにより自動的に生成されるイベントは、この方法では「メッセージの種類」でEN_CHANGEを選択することで、同様のイベントが生成される。

イベントハンドラの追加の様子2

 リンクの起動に関するイベントの実装は以下のとおりである。

void CMFCTestDlg::OnEnLinkRichedit21(NMHDR* pNMHDR, LRESULT* pResult)
{
	ENLINK* pEnLink = reinterpret_cast<ENLINK*>(pNMHDR);
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	*pResult = 0;

	// 左クリック時のみリンクを起動する
	if (pEnLink->msg == WM_LBUTTONDOWN) {
		CRichEditCtrl* richEdit = (CRichEditCtrl*)GetDlgItem(IDC_RICHEDIT21);

		// クリックしたリンクのテキストを取得しリンクを起動する
		CString str;
		richEdit->GetTextRange(pEnLink->chrg.cpMin, pEnLink->chrg.cpMax, str);
		if (ShellExecute(m_hWnd, _T("open"), str, NULL, NULL, SW_SHOW) <= HINSTANCE(32)) {
			AfxMessageBox(_T("エラー"), MB_OK);
		}
	}
}
C++

メニューバー・ツールバー

 ダイアログの領域が圧迫されてきたので、先にメニューバーを作っておく。まずは、リソースビューのソリューションの配下にあるMF[プロジェクト名].rcを右クリックし、リソースの追加を選択する。

メニューバーの追加の様子1

 すると、作成可能なリソースの種類が表示されるので「Menu」を選択肢、「新規作成」を押下する。

メニューバーの追加の様子2

 あとは直観的にメニューバーを構築することができる(ちなみに下画像で表示されている項目以外は作成していない)。Altキーによるショートカットを指定するときは、項目名に「Menu1(&A)」と入力する。この場合はAlt+Aで「Menu1」が開くことを意味する。メニューの「プロパティ」で「区切り」をTrueにすると区切り線が表示される。

メニューバーの構築の様子

 作成したメニューバーをダイアログにドッキングするには、ダイアログのプロパティの「メニューバー」の部分に作成したメニューバーのIDを指定すればいい(下画像でハイライトされている部分)。

メニューバーのドッキングの様子

 ツールバーの作成についてはメニューバーの作成ほど簡単にはいかない。第一の方法としてリソースの追加からツールバーを選択して作成することが考えられるが、これはダイアログにはドッキングすることは(おそらく)できない。また、仮にできたとしてもWindows標準のビットマップ(アイコン)については外部から直接インポートしてこない限り利用することはできない。そのため、プログラム上で作成をする必要がある。

 早速ツールバーを作成してみる。今回はWindows標準のビットマップを利用する。CMFCTestDlgのメンバ変数としてツールバーのハンドルを定義し、その後、実装を行う。ツールバーの構築を行う箇所についてはリッチエディットコントロールでマスクの設定をした場所と同様のCMFCTestDlg::OnInitDialog()で行う。

class CMFCTestDlg : public CDialogEx
{

	// 略

protected:
	// ツールバーのハンドル
	CToolBar m_wndToolBar;


	// 略

};
C++
BOOL CMFCTestDlg::OnInitDialog()
{
	// 略

	if (m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | TBSTYLE_TOOLTIPS)) {
		std::array<TBBUTTON, 10> buttonList = {
			// 新規ファイル作成
			TBBUTTON{ STD_FILENEW , ID_FILE_NEW , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// ファイルを開く
			TBBUTTON{ STD_FILEOPEN , ID_FILE_OPEN , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// ファイルを保存
			TBBUTTON{ STD_FILESAVE , ID_FILE_SAVE , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// 区切り線
			TBBUTTON{ -1 , 0 , TBSTATE_ENABLED , TBSTYLE_SEP },
			// アンドゥ
			TBBUTTON{ STD_UNDO , ID_EDIT_UNDO , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// リドゥ(STD_REDOWはスペルミスだと思う)
			TBBUTTON{ STD_REDOW , ID_EDIT_REDO , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// 区切り線
			TBBUTTON{ -1 , 0 , TBSTATE_ENABLED , TBSTYLE_SEP },
			// コピー
			TBBUTTON{ STD_COPY , ID_EDIT_COPY , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// カット
			TBBUTTON{ STD_CUT , ID_EDIT_CUT , TBSTATE_ENABLED , TBSTYLE_BUTTON },
			// ペースト
			TBBUTTON{ STD_PASTE , ID_EDIT_PASTE , TBSTATE_ENABLED , TBSTYLE_BUTTON }
		};
		CToolBarCtrl& toolBarCtrl = m_wndToolBar.GetToolBarCtrl();

		// アイコンサイズの指定(小(16 x 16)、中(24 x 24)、大(32 x 32))
		toolBarCtrl.SetBitmapSize(CSize(16, 16));
		// ボタンサイズの指定
		toolBarCtrl.SetButtonSize(CSize(20, 20));

		// IDB_STD_SMALL_COLORに対応するWindows標準のアイコンの読み込み
		toolBarCtrl.LoadImages(IDB_STD_SMALL_COLOR, HINST_COMMCTRL);
		// ボタンの追加
		toolBarCtrl.AddButtons(static_cast<int>(buttonList.size()), buttonList.data());

		RepositionBars(AFX_IDW_CONTROLBAR_FIRST, AFX_IDW_CONTROLBAR_LAST, 0);
	}

	return TRUE;
}
C++

 STD_FILENEWといったマクロはWindows標準で与えられているビットマップのインデックスが与えられており、ID_FILE_NEWのようなIDについてのマクロはWindows標準で定義されているコマンドを割り当てている。また、読み込むビットマップについてはHINST_COMMCTRLでWindows標準のものを用いることを宣言し、IDB_STD_SMALL_COLORに対応する画像を読み込んでいる。これらのようなWindows標準のものに対する詳細は以下を参照せよ。

 これを実行すると以下のようなダイアログが表示され、メニューバーとツールバーが作成されていることが確認できる。

メニューバーとツールバーの設置された様子

 次に、設置したメニューバーとツールバーにイベントを設置する。それぞれにおけるイベントはこれまでのような 「ダブルクリックによる追加」や「イベントハンドラーの追加」からは行うことはできない。この場合については 手動で設定をするか、クラスウィザードを介して自動で行う(推奨) という手段ある。
 まず、メニューバーのイベントを 手動で 設置してみる。設置対象はSubMenu1に対してである。まずは、イベントのプロトタイプとその実装を記述し、適当にメッセージボックスを出力するイベントを定義する。

class CMFCTestDlg : public CDialogEx
{

	// 略

	// メニューバーのイベント
	afx_msg void OnCommandMenu1SubMenu1();
};
C++
void CMFCTestDlg::OnCommandMenu1SubMenu1()
{
	AfxMessageBox(_T("[Menu1 > SubMenu1]の実行"), MB_OK | MB_ICONINFORMATION);
}
C++

 後はイベントを作成したイベントをマッピングするだけである。

BEGIN_MESSAGE_MAP(CMFCTestDlg, CDialogEx)

	// 略

	// メニューバーのイベントのマッピング
	ON_COMMAND(ID_MENU1_SUBMENU1, &CMFCTestDlg::OnCommandMenu1SubMenu1)
END_MESSAGE_MAP()
C++

 この段階のプログラムを実行し、メニューバーのSubMenu1を選択することにより、以下のようなメッセージボックスの出現が確認できる。

メニューバーのイベントの発火の様子

 次に、ツールバーのイベントをクラスウィザードを介して自動で 設置してみる。クラスウィザードについては、Visual Studioのメニューバーの「プロジェクト」から起動することができる。そして、以下の画像のように適切なクラス名を指定して、コマンドに対応するIDを指定をすることにより、イベントハンドラを生成することができる。

クラスウィザードからのイベントの追加の様子

 ツールバーのイベントの中身については、ファイルのアイコンがあるのだから、ファイルに関するダイアログのイベントを起動し、その対象となるファイルパスをメッセージボックスで表示させてみる。今回はファイルを開くイベントとファイルを保存する2つのイベントを設置する。そのイベントのプログラムは以下のとおりである。

void CMFCTestDlg::OnCommandFileOpen()
{
	// ファイルを開くダイアログの起動
	CFileDialog dlgFile(TRUE, nullptr, nullptr,
		OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
		// ファイルのフィルターの定義
		_T("Text Files (*.txt)|*.txt|Image Files (*.png; *.jpg)|*.png; *.jpg||"));
	OPENFILENAME& ofn = dlgFile.GetOFN();
	// 複数のファイルを開くことができるようにする
	ofn.Flags |= OFN_ALLOWMULTISELECT;

	// 開くボタンを押下した場合
	if (dlgFile.DoModal() == IDOK) {
		POSITION pos = dlgFile.GetStartPosition();
		// 選択したフィルタのインデックスの取得
		CString indexStr;
		indexStr.Format(_T("選択したインデックス: %d"), ofn.nFilterIndex);
		AfxMessageBox(indexStr, MB_OK | MB_ICONINFORMATION);

		while (pos != nullptr) {
			// 開く対象のファイルパスを取得する
			AfxMessageBox(dlgFile.GetNextPathName(pos), MB_OK | MB_ICONINFORMATION);
		}
	}
}

void CMFCTestDlg::OnCommandFileSave()
{
	// 名前を付けてファイルを保存するダイアログの起動
	CFileDialog dlgFile(FALSE);

	// バッファサイズは十分に大きくとる
	constexpr int bufferSize = MAX_PATH * 2;

	// デフォルトのファイル名を指定する
	CString buffer;
	TCHAR* p = buffer.GetBuffer(bufferSize);
	_stprintf_s(p, bufferSize, _T("デフォルトのファイル名"));
	OPENFILENAME& ofn = dlgFile.GetOFN();
	ofn.lpstrFile = p;
	ofn.nMaxFile = bufferSize;

	// 保存ボタンを押下した場合
	if (dlgFile.DoModal() == IDOK) {
		
		AfxMessageBox(dlgFile.GetPathName(), MB_OK | MB_ICONINFORMATION);
	}

	// メモリ開放
	buffer.ReleaseBuffer();
}
C++

分割ボタン

 分割ボタンの配置については、チェックボックスやラジオボタンと同様に設置することができる。他と異なるのは、ドロップダウンする項目部をメニューバーの要領で作成する必要があることである。メニューバーについては以下のように構築する。「Title」とある部分についてはドロップダウンの項目にならない事に注意する必要がある。

分割ボタンの構築の様子

 あとは、適当に分割ボタンとメニューを関連付けることが考えられるが、以下の方法ではOnInitDialog()終了時に例外が生じてしまう

BOOL CMFCTestDlg::OnInitDialog()
{

	// 略

	// 分割ボタンとメニューを関連付ける
	CSplitButton* splitButton = (CSplitButton*)GetDlgItem(IDC_SPLIT1);
	splitButton->SetDropDownMenu(IDR_MENU2, 0);
	
	return TRUE;
}
C++

 例外が発生してしまう要因は、

指定したコントロールまたは子ウィンドウへのポインター。 パラメーターによって指定された nID 整数 ID を持つコントロールが存在しない場合、値は NULL.
返されるポインターは一時的な場合があり、後で使用するために格納しないでください。

CWnd クラス | Microsoft Docs

とあるように、あくまでもGetDlgItem()で得られるポインタが一時的なものであることが原因であると考えられる(実際のところはわからない)。

 これを解決するには、明示的にIDとメンバ変数を関連付ける操作をすればいい。その手順を示す。分割ボタンに対して右クリックをすると「変数の追加」とあるため、それを選択する。

コントロール変数の追加の様子1

 そうすると、以下のような画面が出現するため、適当に設定を入力し「完了」ボタンを押下する。これにより、ダイアログのクラスにメンバ変数が生成され、DoDataExchange()にデータ交換処理が追記される。

コントロール変数の追加の様子2

 後は、分割ボタンとメニューの関連部を以下のように修正するだけである。

BOOL CMFCTestDlg::OnInitDialog()
{

	// 略

	// 分割ボタンとメニューを関連付ける
	m_splitButton1.SetDropDownMenu(IDR_MENU2, 0);
	
	return TRUE;
}
C++

 そもそもデータ交換の必要があるGUIの要素については変数を作っておいた方がいいというのもある。あまり何度もGetDlgItem()でやるのはよくないだろう(変更の際のリファクタリングが面倒になる)。
 イベントの設置については分割ボタンのボタン部はボタンのイベントと、メニュー部はメニューバーのものと同様に作成が可能であるため、今回は特には作成しない。

コンボボックス

 次は、分割ボタンと見た目がよく似ているコンボボックスを設置してみる。コンボボックスは分割ボタンとは異なり、リストの項目をメニューで管理しないことが特徴である。
 まずは、コンボボックスとスタティックテキストを設置する。今回は、コンボボックスの選択要素が変更されたらその情報をスタティックテキストへ反映するといった処理を実装する。そのため、スタティックテキストにはIDC_STATIC3といったIDを割り振っておく。

コンボボックスの設置の様子1

 コンボボックスの項目の追加については、

コンボボックスの設置の様子2

のようにコンボボックスの「プロパティ」の「データ」からセミコロン区切りで項目を追加をしたり、

BOOL CMFCTestDlg::OnInitDialog()
{

	// 略

	// コンボボックスの項目を追加する
	CComboBox* combo = (CComboBox*)GetDlgItem(IDC_COMBO1);
	combo->AddString(_T("item 3"));
	combo->AddString(_T("item 4"));
	combo->AddString(_T("item 5"));

	return TRUE;
}
C++

のようにダイアログの初期化において実施してもよい。

 イベントの追加については設置したコンボボックスをダブルクリックすることによりイベントハンドラが自動生成されるため、そこに実施内容を記述する。

void CMFCTestDlg::OnCbnSelchangeCombo1()
{
	CComboBox* combo = (CComboBox*)GetDlgItem(IDC_COMBO1);
	CStatic* st = (CStatic*)GetDlgItem(IDC_STATIC3);

	// 選択されたカーソル位置とテキストの取得
	int cursor = combo->GetCurSel();
	CString str;
	combo->GetLBText(cursor, str);

	str.Format(_T("%d: %s"), cursor, static_cast<LPCTSTR>(str));
	st->SetWindowText(str);
}
C++

 かなり素直にイベントを記述できるため簡単である。適当にitem 4を選択した場合の実行結果以下のとおりである。

コンボボックスの操作の様子

タブコントロール

 順番的には他の要素の説明をした方がいいと思うが、ダイアログの領域が狭くなってきたのでタブを作成する。まずは、初期状態で存在している邪魔なスタティックテキストをようやく削除する。

 タブコントロールの作成方法は以下の通りである。

  • タブを設置するダイアログにタブコントロールを設置

  • タブの中身となる子ダイアログを作成

  • タブに子ダイアログを登録

 まずは、ダイアログにタブコントロールを設置する。

タブコントロールの設置の様子

 次に、タブの中身となる子ダイアログを作成する。ダイアログのプロパティとしては、「タイトルバー」をFalse、「スタイル」を子、「境界線」をなし、「静的エッジ」をTrueのように設定する。このようなダイアログを3つ作成する。

タブの中身の子ダイアログの構築の様子1

 次に、ダイアログを示すクラスを作成する。ダイアログを右クリックすると、「クラスの追加」があるため、それをクリックする。

タブの中身の子ダイアログの構築の様子2

 そうしたら以下のようなダイアログが出現するため、適当に項目を設定し、「OK」を押下する。この操作は作成した全てのダイアログについて行う。

タブの中身の子ダイアログの構築の様子3

 後は、作成したタブコントトールに対してタブを与え、そのタブにダイアログを割り当てればいい。まずは、各タブに割り当てるダイアログのハンドルを示すメンバ変数をダイアログのクラスに割り当てる。

class CMFCTestDlg : public CDialogEx
{

	// 略

protected:
	// タブのハンドル
	std::vector<std::unique_ptr<CDialog>> m_tab1Pages;

	// 略
};
C++

 次に、ダイアログの初期化部で前述したとおりの操作を記述する。

BOOL CMFCTestDlg::OnInitDialog()
{

	// 略

	// タブコントロールの登録
	CTabCtrl* tabCtrl = (CTabCtrl*)GetDlgItem(IDC_TAB1);
	tabCtrl->InsertItem(0, _T("タブ1"));
	tabCtrl->InsertItem(1, _T("タブ2"));
	tabCtrl->InsertItem(2, _T("タブ3"));
	CRect r;
	tabCtrl->GetItemRect(0, &r);
	int itemHeight = r.bottom - r.top;
	tabCtrl->GetClientRect(&r);
	// タブのツールチップの高さ分の補正
	r.top += itemHeight + 2;
	// タブにダイアログを割り当てる
	std::pair<int, CDialog*> tabPages[] = {
		{ IDD_TAB1_PAGE1, new CTab1Page1Dlg(tabCtrl) },
		{ IDD_TAB1_PAGE2, new CTab1Page2Dlg(tabCtrl) },
		{ IDD_TAB1_PAGE3, new CTab1Page3Dlg(tabCtrl) }
	};
	for (auto& tabPage : tabPages) {
		m_tab1Pages.emplace_back(tabPage.second);
		m_tab1Pages.back()->Create(tabPage.first, tabCtrl);
		m_tab1Pages.back()->MoveWindow(&r, FALSE);
	}
	// 初期状態のダイアログの指定
	tabCtrl->SetCurSel(0);
	m_tab1Pages[0]->ShowWindow(SW_SHOW);

	return TRUE;
}
C++

 後は、タブの切り替えのイベントの記述である。イベントの追加については他と同様に設置したタブコントロールをダブルクリックすることによりイベントハンドラが自動生成されるため、そこに実施内容を記述する。

void CMFCTestDlg::OnTcnSelchangeTab1(NMHDR* pNMHDR, LRESULT* pResult)
{
	*pResult = 0;

	CTabCtrl* tabCtrl = (CTabCtrl*)GetDlgItem(IDC_TAB1);
	const int j = tabCtrl->GetCurSel();

	for (std::size_t i = 0; i < m_tab1Pages.size(); ++i) {
		m_tab1Pages[i]->ShowWindow(i == j ? SW_SHOW : SW_HIDE);
	}
}
C++

 プログラムを起動し、適当にタブ2を選択すると、以下のようにタブが切り替わりタブコントロールが機能していることが確認できる。

タブ操作の実行の様子

ポップアップメニュー

 ここでのポップアップメニューとは、右クリックで表示させるメニューのことを指す。ポップアップメニューの作成方法は分割ボタンと同様にして、リソースでメニューを作成する。

ポップアップメニューの作成の様子1

 最も安直な方法としては、ダイアログのクラスのメンバ関数であるOnContextMenu()をオーバーライドするという方法である。クラスウィザードからメッセージON_WM_CONTEXTMENUのハンドラを追加する処理を行うことで、OnContextMenu()を自動生成してくれる。

ポップアップメニューの作成の様子2

 イベントの中身のプログラムについては以下の通り。

void CMFCTestDlg::OnContextMenu(CWnd* pWnd, CPoint point)
{
	CMenu Menu;
	if (Menu.LoadMenu(IDR_MENU3)) {
		CMenu* pSubMenu = Menu.GetSubMenu(0);
		if (pSubMenu != NULL) {
			pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
		}
	}
	// メニューの削除はMenuのデストラクタで実施されるため問題なし
}
C++

 これを実行して適当な場所で右クリックをすると、実装をしたポップアップメニューが表示される。

ポップアップメニューの操作の様子

 また、要素が重なっている際のポップアップメニューの表示の優先順位は標準では子のダイアログの方が高く、例えば、タブに埋め込んだダイアログに対してOnContextMenu()を実装すると、子のダイアログのOnContextMenu()が呼び出される。より端的に表現すると、要素の親子関係においてダイアログは自動的に親子関係をもとに子のOnContextMenu()を呼び出すが、ボタンなどに関しては勝手には呼び出されない。そのため、親が明示的にOnContextMenu()の引数の情報をもとに呼び出す必要がある。
 これの実装例については、ツリーコントロールの作成にて行っている。

リストボックス

 リストボックスはコンボボックスをシリアライズしたようなものである。なので設置も非常に容易である。機能的な違いとしては複数の要素を選択できることである。また、リソースエディタから要素の設定をすることはできない。
 今回はリストボックスをタブ1に設置することにする。

リストボックスの追加の様子

 中央にあるボタンと右にあるエディットコントロールについては、リストボックスで何かしら選択されている状態でボタンをクリックするとエディットコントロールに反映するというものを実装するために用いる。

 次に、初期値を設定する。これについては少し注意が必要で、タブ1についてのダイアログ、すなわちCTab1Page1Dlg::OnInitDialog()で行う必要がある。デフォルトの生成ではこれが定義されないため適宜オーバーライド(クラスウィザードから作成可能)して、以下のような実装を記述する。

BOOL CTab1Page1Dlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// リストボックスの項目を追加する
	CListBox* listBox = (CListBox*)GetDlgItem(IDC_LIST1);
	listBox->AddString(_T("item 1"));
	listBox->AddString(_T("item 2"));
	listBox->AddString(_T("item 3"));

	return TRUE;
}
C++

 最後にイベントを設置する。今まで通り、ボタンのクリックイベントハンドラを生成し、イベントの中身を実装をする。その中身は以下のとおりである。

void CTab1Page1Dlg::OnBnClickedButton1()
{
	CListBox* listBox = (CListBox*)GetDlgItem(IDC_LIST1);
	CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT1);

	int cursor = listBox->GetCurSel();

	// 現在選択されている項目が存在するのならばそれをeditに反映する
	if (cursor != LB_ERR) {
		CString str;
		listBox->GetText(cursor, str);
		edit->SetWindowText(str);
	}
}
C++

 プログラムを実行し、リストボックスの要素を選択して矢印ボタンを押下すると、リストボックスの選択された要素のテキストがエディットコントロールへ反映されることが確認できる。

リストボックスの操作の様子

リストコントロール

 リストコントロールはリストボックスを高度にしたものであり、プロパティの「ビュー」で様々な形式で表示を指定することができる。今回はテーブル形式のデータの表示に優れる「レポート」を選択した。また、リストコントロールはタブ3に設置する。スタティックテキストについては現在の選択状況をリアルタイムで反映するための要素である。

リストコントロールの追加の様子

 データの追加については、リストボックスと同様に個別のタブ(今回の場合ならCTab1Page3Dlg::OnInitDialog())に記述をする。

BOOL CTab1Page2Dlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	CListCtrl* listCtrl = (CListCtrl*)GetDlgItem(IDC_LIST1);
	// 拡張スタイルの適用
	listCtrl->SetExtendedStyle(listCtrl->GetExtendedStyle() |
		// 罫線の表示
		LVS_EX_GRIDLINES |
		// 行単位で選択する
		LVS_EX_FULLROWSELECT |
		// データはなくてもヘッダを表示するようにする
		LVS_EX_HEADERINALLVIEWS);

	// 列を設定する
	listCtrl->InsertColumn(0, _T("column 1"), LVCFMT_LEFT);
	listCtrl->InsertColumn(1, _T("column 2"), LVCFMT_LEFT);
	listCtrl->InsertColumn(2, _T("column 3"), LVCFMT_LEFT);
	// 列幅の自動設定するようにする
	listCtrl->SetColumnWidth(0, LVSCW_AUTOSIZE_USEHEADER);
	listCtrl->SetColumnWidth(1, LVSCW_AUTOSIZE_USEHEADER);
	listCtrl->SetColumnWidth(2, LVSCW_AUTOSIZE_USEHEADER);

	// 行の挿入
	listCtrl->InsertItem(0, _T("data 1-1"));
	listCtrl->SetItemText(0, 1,_T("data 1-2"));
	listCtrl->SetItemText(0, 2, _T("data 1-3"));
	// 行の挿入
	listCtrl->InsertItem(1, _T("data 2-1"));
	listCtrl->SetItemText(1, 1, _T("data 2-2"));
	listCtrl->SetItemText(1, 2, _T("data 2-3"));
	// 行の挿入
	listCtrl->InsertItem(2, _T("data 3-1"));
	listCtrl->SetItemText(2, 1, _T("data 3-2"));
	listCtrl->SetItemText(2, 2, _T("data 3-3"));

	return TRUE;
}
C++

 リストコントロールの定義の冒頭で行っている拡張スタイルの詳細については以下を参照せよ。

 イベントの追加については他と同様に設置したリストコントロールをダブルクリックすることによりイベントハンドラが自動生成されるため、そこに実施内容を記述する。

void CTab1Page2Dlg::OnLvnItemchangedList1(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
	*pResult = 0;

	CListCtrl* listCtrl = (CListCtrl*)GetDlgItem(IDC_LIST1);
	CStatic* st = (CStatic*)GetDlgItem(IDC_STATIC2);

	CString str;

	// 全ての選択された要素を探索するしてスタティックテキストへ反映する
	UINT cnt = listCtrl->GetSelectedCount();
	int j = -1;
	for (UINT i = 0; i < cnt; ++i) {
		j = listCtrl->GetNextItem(j, LVNI_SELECTED);
		CString temp;
		temp.Format(_T("%d行目を選択\n"), j);
		str += temp;
	}
	st->SetWindowText(str);
}
C++

 また、ヘッダをクリックした際のイベントも定義してみる。ヘッダのクリックについては「イベントハンドラーの追加」からLVN_COLUMNCLICKを指定してイベントを定義する。

リストコントロールのイベントハンドラの追加の様子

 今回実装する動作としては、ヘッダをクリックした際は行の選択を全て解除して、スタティックテキストに選択した列の内容を反映する。

void CTab1Page2Dlg::OnLvnColumnclickList1(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
	*pResult = 0;

	CListCtrl* listCtrl = (CListCtrl*)GetDlgItem(IDC_LIST1);
	CStatic* st = (CStatic*)GetDlgItem(IDC_STATIC2);

	// 全ての選択された対象を解除する
	UINT cnt = listCtrl->GetSelectedCount();
	int j = -1;
	for (UINT i = 0; i < cnt; ++i) {
		j = listCtrl->GetNextItem(j, LVNI_SELECTED);
		listCtrl->SetItemState(j, 0, LVNI_SELECTED | LVNI_FOCUSED);
	}

	// 選択した列の情報をスタティックテキストへ反映する(listCtrl->GetSelectedColumn()では取得できない)
	CString str;
	str.Format(_T("%d列目を選択"), pNMLV->iSubItem);
	st->SetWindowText(str);
}
C++

 プログラムを実行して行を選択すると、その情報がスタティックテキストへ反映されることがわかる。また、その状態から列を選択すると行の選択状態がクリアされ、列の選択に関する情報が表示されることが確認できる。

リストコントロールの操作の様子1
リストコントロールの操作の様子2

ツリーコントロール

 ツリーコントロールはリストボックスに階層構造を持たせる方向で高度にしたものである。プロパティについては、ツリーコントロールの階層構造を開閉ボタンの設置のために「ボタンあり」をTrue、階層構造に関する罫線を表示するために「行あり」をTrueに設定する。

ツリーコントロールの設置の様子

 ツリーコントロールを右クリックによるポップアップメニューを用いて制御を行いたいため、出現させるメニューを定義する。今回はツリーに対して要素の挿入と削除を行うために、それに関する項目を定義する。また、「削除」に関するIDはID_MYTREE_DELETE、「次に挿入」に関するIDはID_MYTREE_INSERT_NEXT、「子に挿入」に関するIDはID_MYTREE_INSERT_CHILDのようにした。

ツリーコントロールの操作のためのポップアップメニューの設定の様子

 ツリーコントロールを設置するダイアログにWM_CONTEXTMENUに関するハンドラを生成する。その中身は以下のように定義する。TrackPopupMenu()の第4引数にはtreeCtrlを指定したいところではあるが、今回はポップアップメニューのイベントのキャッチをツリーコントロールではなくダイアログCTab1Page3Dlgで行うためthisを指定している。

void CTab1Page3Dlg::OnContextMenu(CWnd* pWnd, CPoint point)
{
	CTreeCtrl* treeCtrl = (CTreeCtrl*)GetDlgItem(IDC_TREE1);

	// ターゲットとなる対象がツリーコントロールの場合の処理
	if (pWnd == treeCtrl) {
		// 右クリックした位置にツリーの要素が存在すれば選択する
		if (point != CPoint(-1, -1)) {
			CPoint p = point;
			treeCtrl->ScreenToClient(&p);

			UINT flags = 0;
			HTREEITEM hit = treeCtrl->HitTest(p, &flags);
			if (hit != nullptr) treeCtrl->SelectItem(hit);
		}
		// メニューを作成する
		CMenu Menu;
		if (Menu.LoadMenu(IDR_MENU4)) {
			CMenu* pSubMenu = Menu.GetSubMenu(0);
			if (pSubMenu != NULL) {
				pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
			}
		}
	}
	// デフォルトの処理(親のOnContextMenu())を呼び出す
	else {
		CDialogEx::OnContextMenu(pWnd, point);
	}
}
C++

 これにより、ツリーコントロール上で右クリックをすると専用のポップアップメニューが出現する。

ツリーコントトール上でのポップアップメニューの表示の様子

 次に設置したポップアップメニューのイベントを設置する。イベントの設置方法についてはメニューバーと同様にクラスウィザードから作成することができる。

ツリーコントトール上でのポップアップメニューに関するイベントの追加の様子

 後は、具体的にツリーに要素を挿入するロジックを定義する。

// 選択されたツリー要素の次の要素にツリー要素を挿入
void CTab1Page3Dlg::OnMytreeInsertNext()
{
	static int num = 0;
	CTreeCtrl* treeCtrl = (CTreeCtrl*)GetDlgItem(IDC_TREE1);
	CString str;
	str.Format(_T("next %d"), num++);

	HTREEITEM hItem = treeCtrl->GetSelectedItem();
	// 選択したアイテムが存在しなければルート要素を親として挿入
	if (hItem == nullptr) {
		treeCtrl->InsertItem(str);
	}
	// 選択した次の要素に挿入
	else {
		HTREEITEM parent = treeCtrl->GetParentItem(hItem);
		treeCtrl->InsertItem(str, parent, hItem);
	}
}

// 選択されたツリー要素の子にツリー要素を挿入
void CTab1Page3Dlg::OnMytreeInsertChild()
{
	static int num = 0;
	CTreeCtrl* treeCtrl = (CTreeCtrl*)GetDlgItem(IDC_TREE1);
	CString str;
	str.Format(_T("child %d"), num++);

	HTREEITEM hItem = treeCtrl->GetSelectedItem();
	// 選択したアイテムが存在しなければエラー
	if (hItem == nullptr) {
		AfxMessageBox(_T("子の追加には対象となるノードの選択が必須です"), MB_OK);
	}
	// 選択した要素の子に挿入
	else {
		treeCtrl->InsertItem(str, hItem);
		// ツリーを展開しておく
		treeCtrl->Expand(hItem, TVE_EXPAND);
	}
}

// 選択されたツリー要素を削除
void CTab1Page3Dlg::OnMytreeDelete()
{
	CTreeCtrl* treeCtrl = (CTreeCtrl*)GetDlgItem(IDC_TREE1);

	HTREEITEM hItem = treeCtrl->GetSelectedItem();
	// 選択したアイテムが存在しなければエラー
	if (hItem == nullptr) {
		AfxMessageBox(_T("ノードの削除には対象となるノードの選択が必須です"), MB_OK);
	}
	// 選択した要素の削除を試みる
	else {
		// 子を持つならば子を含めて削除するかを問う
		if (treeCtrl->ItemHasChildren(hItem)) {
			if (IDYES == AfxMessageBox(_T("子も削除しますか"), MB_YESNO)) {
				treeCtrl->DeleteItem(hItem);
			}
		}
		// 子を持たないならばそのまま削除する
		else {
			treeCtrl->DeleteItem(hItem);
		}
	}
}
C++

 以上でツリーを構築する機能の構築が完了したため、自由にツリーを構築することができる。

ツリーコントロールの操作の様子

ダイアログの制御

 これまででは1つのウィンドウ(ダイアログ)を扱ってきたが、複数のウィンドウを扱う方法について扱う。

モーダルダイアログ

 まずは、もっとも単純なウィンドウであるモーダルダイアログを表示させてみる。初めに、モーダルダイアログ専用のダイアログおよびクラスを作成する。

モーダルダイアログの作成の様子1
モーダルダイアログの作成の様子2

 どのイベントによりモーダルダイアログを表示するかであるが、今回は分割ボタンの要素1(DropDown1)を選択したときにモーダルダイアログを表示させるようにする。イベントハンドラの実装についてはこれまで通り、クラスウィザードからイベントハンドラを生成し、その中身を記述する。

モーダルダイアログのイベントの追加の様子
void CMFCTestDlg::OnTitleDropdown1()
{
	CModalDlg dlg(this);
	CString str;

	// モーダルダイアログの表示とその終了コードの取得
	INT_PTR code = dlg.DoModal();
	switch (code) {
	case -1:
		AfxMessageBox(_T("ダイアログの生成に失敗"));
		break;
	case IDOK:
		AfxMessageBox(_T("OKボタンを押下"));
		break;
	case IDCANCEL:
		AfxMessageBox(_T("キャンセルボタンを押下"));
		break;
	case IDABORT:
		str.Format(_T("エラーが発生(エラーコード: %lu)"), GetLastError());
		AfxMessageBox(str);
	default:
		str.Format(_T("その他の終了コード(%lld)"), code);
		AfxMessageBox(str);
		break;
	};
}
C++

 後は、プログラムを実行し、分割ボタンの要素1(DropDown1)を選択すればモーダルダイアログを表示されることが確認できる。

モーダルダイアログの操作の様子1

 また、モーダルダイアログのキャンセルボタンやバツボタンを押下すれば、キャンセルに関する終了コードが取得されていることが確認できる。

モーダルダイアログの操作の様子2

モードレスダイアログ

 次はモードレスダイアログを作成してみる。モーダルダイアログの場合と同様にして専用のダイアログおよびクラスを作成する。

モードレスダイアログの作成の様子1
モードレスダイアログの作成の様子2

 モードレスダイアログの表示についてはモードレスダイアログと同様にして、分割ボタンの要素2(DropDown2)を選択したときにモードレスダイアログを表示させるようにする。

モードレスダイアログのイベントの追加の様子

 イベントハンドラについてはモーダルダイアログとは異なる部分が多い。なぜならば、モードレスダイアログを管理は親クラスなどがする必要があるためである(モーダルダイアログのダイアログのハンドラはスタックで管理される)ことや、モーダルダイアログとは異なり同期的ではないためである。
 まずは、モードレスダイアログを管理する変数を与える。

class CMFCTestDlg : public CDialogEx
{

	// 略

protected:
	// モードレスダイアログのハンドル
	std::unique_ptr<CDialog> m_modelessDlg;


	// 略

};
C++

 ダイアログの表示のためのコードは以下のようになる。

void CMFCTestDlg::OnTitleDropdown2()
{
	// ダイアログが存在しない場合にのみ生成
	if (!m_modelessDlg) {
		m_modelessDlg.reset(new CModelessDlg);
		m_modelessDlg->Create(IDD_DIALOG3);
		m_modelessDlg->ShowWindow(SW_SHOW);
	}
	// 通常ではダイアログは非表示になるだけであるため表示するだけでよい
	else {
		m_modelessDlg->ShowWindow(SW_SHOW);
	}
}
C++

 これにより、分割ボタンの要素2(DropDown2)を選択したときにモードレスダイアログが表示され、削除も機能しているかのように見える。しかし、コメントにもあるように、モードれすダイアログはメインのウィンドウではないため、例えばキャンセルボタンを押下しても、単にモーダルダイアログが非表示になるだけである

 モードレスダイアログを削除する際には、親で管理しているダイアログのハンドルのリソースも適切に開放するべきである。CWnd::DestroyWindow()のドキュメントによれば、削除の通知はWM_PARENTNOTIFYメッセージが親で受信できるとあるが、今回の場合は受信することはできない。なぜならば、厳密にはダイアログのプロパティの「スタイル」に子を指定した場合に送信される(と思われる)ものであるためである
 今回は安直に親に対して専用の通知を送信することにより、リソースの開放を行うことにする。まずは、専用のコマンドIDを定義したいところであるが、以下のようにResource.hを直接書き換えると、Visual Studioが勝手に書き換えた内容をなかったことにするため、ダミーのメニューバーなどを作成し、IDを生成する

#define ID_DESTROY_MODELESS_DLG			/* 重複のない適当な値 */
C++

 以下がそのダミーを作成する様子。

ID生成のためのダミーのメニューバーの作成の様子

 イベントハンドラの定義についてもこれまでと同様にしてクラスウィザードから定義をし、中身についてはダイアログのインスタンスを削除する処理を記述する。

モードレスダイアログの削除イベントの定義の様子
void CMFCTestDlg::OnDestroyModelessDlg()
{
	// ダイアログの削除
	if (m_modelessDlg) {
		// デストラクタで開放してくれればいいものの、警告を出してくるので明示的にDestroyWindow()を呼び出す
		m_modelessDlg->DestroyWindow();
		m_modelessDlg.reset();
	}
}
C++

 後は、モードレスダイアログの「OK」ボタンを押下したタイミングでコマンドを送信すればいい。

void CModelessDlg::OnBnClickedOk()
{
	CDialogEx::OnOK();
	// ウィンドウのリソースを破棄する(DestroyWindow()の代わりに以下を実行することで親がもつハンドルの整合性を保つ)
	GetParent()->SendMessage(WM_COMMAND, ID_DESTROY_MODELESS_DLG);
}
C++

 以上により、モードレスダイアログの削除を機能させることができる(そもそもダイアログ生成時に親を指定しないでおいてドキュメントに従ってdelete thisをするという手段もあるにはある)。
 しかし、モードレスダイアログ起動時にモーダルダイアログを立ち上げるとモードレスダイアログが操作できてしまうという問題が残っている。それに関しては、モーダルダイアログの生成・削除の前後でモードレスダイアログを無効化する処理を挿入することで解決をすることができる。

void CMFCTestDlg::OnTitleDropdown1()
{
	CModalDlg dlg(this);
	CString str;

	// モードレスダイアログの無効化
	if (m_modelessDlg) {
		m_modelessDlg->EnableWindow(FALSE);
	}

	// モーダルダイアログの表示とその終了コードの取得
	INT_PTR code = dlg.DoModal();
	switch (code) {
		// 略
	};

	// モードレスダイアログの有効化
	if (m_modelessDlg) {
		m_modelessDlg->EnableWindow(TRUE);
	}
}
C++

ショートカットキー

 ショートカットキーの実装はアクセラレータを利用することで達成することができる。まずはリソースの追加からアクセラレータを選択し、「新規作成」を押下する。

アクセラレータの定義の様子1

 あとはエディタから直観的にショートカットキーの設定をすることができる。IDについてはこれまで作成したコマンドについてのIDを指定する。タイプについては仮想キー(VIRTKEY)か物理キー(ASCII)のどちらのキー配列を用いるかの指定をする。

アクセラレータの定義の様子2

 次に、ダイアログの初期化部においてアクセラレータのハンドルを取得する処理を記述する。

// CMFCTestDlg ダイアログ
class CMFCTestDlg : public CDialogEx
{

	// 略

protected:
	// アクセラレータのハンドル
	HACCEL m_hAccel;

	// 略
};
C++
BOOL CMFCTestDlg::OnInitDialog()
{

	// 略

	// アクセラレータの構築
	m_hAccel = ::LoadAccelerators(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_ACCELERATOR1));

	return TRUE;
}
C++

 後は、アクセラレータをハンドルするためにPreTranslateMessage()をオーバーライドして、その中身を記述する。

イベントの転送をトラップするイベントの追加の様子
BOOL CMFCTestDlg::PreTranslateMessage(MSG* pMsg)
{
	if (pMsg) {
		// メッセージが送信される前にアクセラレータを検査する
		if (m_hAccel) {
			if (::TranslateAccelerator(GetSafeHwnd(), m_hAccel, pMsg)) return TRUE;
		}
	}

	return CDialogEx::PreTranslateMessage(pMsg);
}
C++

 プログラムを立ち上げ、「Ctrl+Alt+F1」と入力するとモーダルダイアログが立ち上がるし、「Ctrl+Q」と入力するとメインのウィンドウの「ボタン」ボタンを押下した際のイベントが発火する。

イベントの発火

 イベントを発火させることについてはモードレスダイアログの方で若干触れたが、改めてその方法について述べておく(もちろん、イベントの定義された関数を直接呼び出してもよいが、今回はWindows APIにおけるイベント管理方式であるメッセージキューを介して何とかする事について述べる)。

 モードレスダイアログの削除の際に親に送信したように、送信先となるターゲットとなるウィンドウのメンバ関数として、SendMessage()を呼び出すことによりメッセージを送信することができる。

GetParent()->SendMessage(WM_COMMAND, ID_DESTROY_MODELESS_DLG);
C++

 SendMessage()の第1引数(message)にはメッセージの種類(ボタンのクリックなど)、第2引数(wParam)と第3引数(lParam)にはメッセージ固有の詳細情報を記述する。wParamlParamの違いとしては、wParamにはハンドラを示すIDをが指定され、lParamにはメッセージとして渡されるデータ(構造体へのポインタ等)が指定されるという点で異なる。
 具体的に指定するlParamについては、以下のドキュメントから個別のメッセージを定義を読むといいだろう。WM_でタイトルにフィルターをかけるとメッセージの種類一覧が表示されるため、そこから検索をかけるとよい。

おわりに

 とりあえずダイアログアプリケーションを作成する際のMFCの基礎的な部分についてはおおよそ触ることができたと思われる。感想としては、隠しきれないWindows API感がけっこうアレというのと、C++はC++でもC++03以前の古いBetter Cだなと思った。
 あとは、SDIやMDIといったViewを用いたアプリケーション作成といったところではあるが、基本的なGUI等の操作は変わらなかったり、パーツとしてダイアログを用いたりするため、どうしてもダイアログの扱いが初めに必要になってしまう。これらを扱おうとするだけで非常に記事が長くなってしまうため、今回はなしとした。