車輪の再発明 : MFC CDialog に関するTips

# 1. はじめに
CDialog に関する情報があちこちにバラバラにおいてあるのでまとめてみました。
クラスウィザードの使い方とかはわかっている人向け。自分用メモ。

こんなダイアログを目標に作っていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305607/47657fbd-2cbd-48d3-9223-8e9053de04e8.png)

動作仕様としてはこんな感じ
1. Enter と　Esc を無視したい
2. [Start] をクリックしたら別スレッドで適当な処理をする。ここではプログレスバーが増えるだけ
3. プログレスバーは10段階。1秒で1増えて、10秒で終了
4. 処理をしている間は　閉じる[X] を禁止したい。[Start]も押せないようにしたい
5. 処理をしている間は ぐるぐるカーソル表示

長い記事になりますが、ご容赦を。

# 2. ということで順番に作っていきます。
## 2-1. Enter と　Esc を無視したい
Enter、 Esc でダイアログが閉じるのは仕様なのですけど、凝ったダイアログを作っていると無視したいことがあります。

以下に情報があります。
https://www.paveway.info/entry/2019/02/28/mfc_notclose

クラスウィザードから PreTranslateMessage をオーバーライドします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305607/38b04153-24e8-4c65-931f-d855aa8fabdb.png)

コード。

``` C++:抜粋1.cpp

BOOL CThreadTestDlg::PreTranslateMessage(MSG* pMsg)
{
	// TODO: ここに特定なコードを追加するか、もしくは基底クラスを呼び出してください。
	if (WM_KEYDOWN == pMsg->message)
	{
		switch (pMsg->wParam)
		{
		case VK_RETURN:
			return FALSE;
		case VK_ESCAPE:
			return FALSE;
		default:
			break;
		}
	}
	return CDialogEx::PreTranslateMessage(pMsg);
}

```
## 2-2. プログレスバーの処理

ここでは ID を IDC_PROGRESS_TEST コントロール変数を m_Progress とします。
ダイアログにプログレスバーを配置したら、コントロール変数を割り当てます。

ヘッダに以下、
``` C++:抜粋2.h
CProgressCtrl	m_Progress;
```

cpp に以下が付加されます。

``` C++:抜粋2.cpp
void CThreadTestDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_PROGRESS_TEST, m_Progress); // <-これ
}

```

プログレスバーに対し、範囲とを現在位置(0)を設定します。
具体的には OnInitDialog に書きます。後で値をスレッドからもらうように変更します。

``` C++:Oninitdialog

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

    // ～ いろいろ省略 ～

	// TODO: 初期化をここに追加します。
	m_Progress.SetRange(0, 10);
	m_Progress.SetPos(0);

	return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

```
## 2-3. [Start]ボタンの処理
[Start]ボタンを押せるようにしたり、押せないようにしたりしたいので、コントロール変数を追加します。

### 2-3-1. コントロール変数とイベントハンドラの追加

同様にヘッダとcppに以下。

``` C++:抜粋3.h
CButton			m_ButtonStart;
```
``` C++:抜粋3.cpp
void CThreadTestDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_PROGRESS_TEST, m_Progress);
	DDX_Control(pDX, IDC_BUTTON_START, m_ButtonStart); // <- 追加分
}

```
[Start] をクリックしたら～ の部分はイベントハンドラを登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305607/b6c0398f-31ca-4ffe-b067-31f825f1c664.png)

登録すると以下のようになります。

``` C++:ヘッダ
	afx_msg void OnBnClickedButtonStart();
```

 ``` C++:ソース
BEGIN_MESSAGE_MAP(CThreadTestDlg, CDialogEx)
	ON_BN_CLICKED(IDC_BUTTON_START, &CThreadTestDlg::OnBnClickedButtonStart)
END_MESSAGE_MAP()

void CThreadTestDlg::OnBnClickedButtonStart()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
}

```
### 2-3-2. ダイアログコントロールの有効化/無効化
処理をしている間は　閉じる[X] を禁止したい。[Start]も押せないようにしたい。
ので、一連の処理を行う別関数を作ります。

[X] ボタンの禁止方法は以下にあります。
https://www.paveway.info/entry/2019/02/11/mfc_disableclosebutton
    
``` C++:ヘッダ
	void			EnableControl(BOOL b);
```

``` C++:ソース
void	CThreadTestDlg::EnableControl(BOOL b)
{
	CMenu* pMenu = GetSystemMenu(FALSE);
	if (b) {
		// 有効
		pMenu->EnableMenuItem(SC_CLOSE, MF_DEFAULT);
	}
	else {
		// 無効
		pMenu->EnableMenuItem(SC_CLOSE, MF_GRAYED);
	}
	m_ButtonStart.EnableWindow(b);
}
```
## 2-4. 裏処理
スレッドを作りましょう。スレッド用のクラスは別に作ったのでそれを利用します。

https://qiita.com/macchan/items/d6fa97791287c13115dc

しょうもないクラスですけど、排他処理だけはしっかりやりましょう。

``` C++:ThreadMain.h
// ThreadMain.h
#pragma once

#include	"ThreadBase.h"
#include	<mutex>

class CThreadMain : public CThreadBase
{
public:
	CThreadMain();
	virtual ~CThreadMain();

	HANDLE CreateThread();

	long	GetMax() {
		return m_nMax;
	}

	long	GetCur() {
		std::lock_guard<std::mutex> lock(m_mutex);
		return m_nCur;
	}

private:
	static unsigned __stdcall Run(void* pParam);

	void	SetCur(long cur) {
		std::lock_guard<std::mutex> lock(m_mutex);
		m_nCur = cur;
	}


	long				m_nCur;
	static const long	m_nMax;
	std::mutex			m_mutex;
};
```

``` C++:ThreadMain.cpp
// ThreadMain.cpp
#include	"pch.h"
#include	"ThreadMain.h"

const long	CThreadMain::m_nMax = 10;

CThreadMain::CThreadMain()
	: m_nCur(0)
{
	// no more!
}

CThreadMain::~CThreadMain()
{
	// no more!
}

HANDLE CThreadMain::CreateThread()
{
    if(isThreadRunning()) {
        return INVALID_HANDLE_VALUE;
    }
	SetCur(0);    
	return __super::CreateThread(CThreadMain::Run, this);
}

unsigned __stdcall CThreadMain::Run(void* pParam)
{
	CThreadMain* pThis = reinterpret_cast<CThreadMain*>(pParam);

	const long  nMax = pThis->GetMax();
	long nCur = pThis->GetCur();
	while ( nCur < nMax) {
		::Sleep(1000);
		pThis->SetCur(++nCur);
	}
	return 0;
}

```
## 2-5. タイマ
裏でスレッドが回っている間、タイマで時々スレッドの進捗を監視して、プログレスバーの更新をします。

まずはタイマの作り方。

### 2-5-1. タイマを作る

``` C++:ソース
	m_uTimerID = SetTimer(ID, tim, NULL);
```

IDは作りたいタイマのID。タイマが複数あったら他と被らないようにします。実際に作ったタイマIDが(0以外の)戻り値で返されるので、以降はこれで管理します。timは間隔でミリ秒単位。精度は「動くだけマシ」な程度。最後の引数はタイマ処理をする関数ポインタで、NULLを与えるとデフォルトの OnTimer 関数が呼ばれるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305607/cb6d22ff-8b96-4a05-a315-6d9dcf0eb797.png)

1秒ごとに更新だから、半分の500ミリ秒で監視したらええんちゃうの。
スレッドもついでに作っちゃいましょう。

``` C++:ヘッダ
    CThreadMain     		m_Thread;
    static const UINT_PTR	m_cuTimerID;
	UINT_PTR				m_uTimerID;
```

``` C++:ソース
const UINT_PTR	CThreadTestDlg::m_cuTimerID = 1;

CThreadTestDlg::CThreadTestDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_THREADTEST_DIALOG, pParent)
	, m_uTimerID(0) // <- 初期化
{
    // 省略
}

void CThreadTestDlg::OnBnClickedButtonStart()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
    // 操作不可にする
    EnableControl(FALSE);

	// スレッドの生成
	m_Thread.CreateThread();

	// タイマの生成
	m_uTimerID = SetTimer(m_cuTimerID, 500, NULL);
}

void CThreadTestDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: ここにメッセージ ハンドラー コードを追加するか、既定の処理を呼び出します。
	CDialogEx::OnTimer(nIDEvent);
}

```
### 2-5-2. タイマ内での処理
タイマ内では スレッドから進捗状況をもらってプログレスバーを更新します。
スレッドが終了したらタイマを止めてメッセージを表示します。操作禁止にしたコントロールを許可します。OnTimerにはタイマIDがやってくるので、作ったタイマIDと比較します。

``` C++:ソース
void CThreadTestDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: ここにメッセージ ハンドラー コードを追加するか、既定の処理を呼び出します。
	if (m_uTimerID == nIDEvent) {
		const long cur = m_Thread.GetCur();
		m_Progress.SetPos(cur);

		if (!m_Thread.isThreadRunning()) {
            m_Thread.join();
			StopTimer();

			AfxMessageBox(_T("Complete!"), MB_ICONINFORMATION);

			EnableControl(TRUE);
			m_Progress.SetPos(0);
		}
		return;
	}
	CDialogEx::OnTimer(nIDEvent);
}
```

### 2-5-3. タイマを止める
StopTimer() の中身は KillTimer です。

``` C++:ヘッダ
	void			StopTimer();
```

``` C++:ソース
void	CThreadTestDlg::StopTimer()
{
	if (m_uTimerID != 0) {
		KillTimer(m_uTimerID);
		m_uTimerID = 0;
	}
}
``` 
## 2-6. カーソル
CWaitCursorだとマルチスレッドに対応できないです。
「処理をしている間はぐるぐるカーソルを表示」の情報はなかなか見つけにくいです。

答: WM_SETCURSOR イベントハンドラを作りましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305607/9a62bbcd-3d72-40c9-8718-f15c5bda5f5e.png)

OnSetCursor で スレッドが動いていたら
+ BeginWaitCursor()
+ TRUE を返す
この2点です。

``` C++:ソース
BOOL CThreadTestDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
	// TODO: ここにメッセージ ハンドラー コードを追加するか、既定の処理を呼び出します。
	if (m_Thread.isThreadRunning()) {
		BeginWaitCursor();
		return TRUE;
	}

	return CDialogEx::OnSetCursor(pWnd, nHitTest, message);
}

```
# 3. まとめ
Aboutは余計なので削りました。まとめてヘッダとソースを置きます。

``` C++:ヘッダ

// ThreadTestDlg.h : ヘッダー ファイル
//

#pragma once

#include	"ThreadMain.h"

// CThreadTestDlg ダイアログ
class CThreadTestDlg : public CDialogEx
{
// コンストラクション
public:
	CThreadTestDlg(CWnd* pParent = nullptr);	// 標準コンストラクター

// ダイアログ データ
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_THREADTEST_DIALOG };
#endif

private:
	virtual void DoDataExchange(CDataExchange* pDX) override;	// DDX/DDV サポート
	virtual BOOL OnInitDialog() override;
	virtual BOOL PreTranslateMessage(MSG* pMsg) override;

// 実装
private:
	HICON m_hIcon;

	// 生成された、メッセージ割り当て関数
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg void OnBnClickedButtonStart();
	afx_msg void OnTimer(UINT_PTR nIDEvent);
	afx_msg HCURSOR OnQueryDragIcon();
	afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
	DECLARE_MESSAGE_MAP()

private:
	void			StopTimer();
	void			EnableControl(BOOL b);

	CProgressCtrl	m_Progress;
	CButton			m_ButtonStart;
	CThreadMain		m_Thread;

	static const UINT_PTR	m_cuTimerID;
	UINT_PTR				m_uTimerID;
};

```

``` C++:ソース

// ThreadTestDlg.cpp : 実装ファイル
//

#include "pch.h"
#include "framework.h"
#include "ThreadTest.h"
#include "ThreadTestDlg.h"
#include "afxdialogex.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CThreadTestDlg ダイアログ

const UINT_PTR	CThreadTestDlg::m_cuTimerID = 1;

CThreadTestDlg::CThreadTestDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_THREADTEST_DIALOG, pParent)
	, m_uTimerID(0)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CThreadTestDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_PROGRESS_TEST, m_Progress);
	DDX_Control(pDX, IDC_BUTTON_START, m_ButtonStart);
}

BEGIN_MESSAGE_MAP(CThreadTestDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDC_BUTTON_START, &CThreadTestDlg::OnBnClickedButtonStart)
	ON_WM_TIMER()
	ON_WM_SETCURSOR()
END_MESSAGE_MAP()


// CThreadTestDlg メッセージ ハンドラー
BOOL CThreadTestDlg::PreTranslateMessage(MSG* pMsg)
{
	// TODO: ここに特定なコードを追加するか、もしくは基底クラスを呼び出してください。
	if (WM_KEYDOWN == pMsg->message)
	{
		switch (pMsg->wParam)
		{
		case VK_RETURN:
			return FALSE;
		case VK_ESCAPE:
			return FALSE;
		default:
			break;
		}
	}
	return CDialogEx::PreTranslateMessage(pMsg);
}

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

	// "バージョン情報..." メニューをシステム メニューに追加します。

	// IDM_ABOUTBOX は、システム コマンドの範囲内になければなりません。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != nullptr)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// このダイアログのアイコンを設定します。アプリケーションのメイン ウィンドウがダイアログでない場合、
	//  Framework は、この設定を自動的に行います。
	SetIcon(m_hIcon, TRUE);			// 大きいアイコンの設定
	SetIcon(m_hIcon, FALSE);		// 小さいアイコンの設定

	// TODO: 初期化をここに追加します。
	m_Progress.SetRange(0, m_Thread.GetMax());
	m_Progress.SetPos(0);

	return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}

void CThreadTestDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
		CAboutDlg dlgAbout;
		dlgAbout.DoModal();
	}
	else
	{
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

// ダイアログに最小化ボタンを追加する場合、アイコンを描画するための
//  下のコードが必要です。ドキュメント/ビュー モデルを使う MFC アプリケーションの場合、
//  これは、Framework によって自動的に設定されます。

void CThreadTestDlg::OnPaint()
{
	if (IsIconic())
	{
		CPaintDC dc(this); // 描画のデバイス コンテキスト

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// クライアントの四角形領域内の中央
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// アイコンの描画
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
		CDialogEx::OnPaint();
	}
}

// ユーザーが最小化したウィンドウをドラッグしているときに表示するカーソルを取得するために、
//  システムがこの関数を呼び出します。
HCURSOR CThreadTestDlg::OnQueryDragIcon()
{
	return static_cast<HCURSOR>(m_hIcon);
}

void CThreadTestDlg::OnBnClickedButtonStart()
{
	// TODO: ここにコントロール通知ハンドラー コードを追加します。
	EnableControl(FALSE);

	// start thread
	m_Thread.CreateThread();

	// use default OnTimer
	m_uTimerID = SetTimer(m_cuTimerID, 500, NULL);
}

void	CThreadTestDlg::EnableControl(BOOL b)
{
	CMenu* pMenu = GetSystemMenu(FALSE);
	if (b) {
		// 有効
		pMenu->EnableMenuItem(SC_CLOSE, MF_DEFAULT);
		m_ButtonStart.EnableWindow(TRUE);
	}
	else {
		// 無効
		pMenu->EnableMenuItem(SC_CLOSE, MF_GRAYED);
		m_ButtonStart.EnableWindow(FALSE);
	}
}

void	CThreadTestDlg::StopTimer()
{
	if (m_uTimerID != 0) {
		KillTimer(m_uTimerID);
		m_uTimerID = 0;
	}
}

void CThreadTestDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: ここにメッセージ ハンドラー コードを追加するか、既定の処理を呼び出します。
	if (m_uTimerID == nIDEvent) {
		const long cur = m_Thread.GetCur();
		m_Progress.SetPos(cur);

		if (!m_Thread.isThreadRunning()) {
			StopTimer();
			m_Thread.join();

			AfxMessageBox(_T("Complete!"), MB_ICONINFORMATION);

			EnableControl(TRUE);
			m_Progress.SetPos(0);
		}
		return;
	}
	CDialogEx::OnTimer(nIDEvent);
}

BOOL CThreadTestDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
	// TODO: ここにメッセージ ハンドラー コードを追加するか、既定の処理を呼び出します。
	if (m_Thread.isThreadRunning()) {
		BeginWaitCursor();
		return TRUE;
	}

	return CDialogEx::OnSetCursor(pWnd, nHitTest, message);
}

```
# 4. 最後に
この記事がお役に立てば幸いです。