1. はじめに
CDialog に関する情報があちこちにバラバラにおいてあるのでまとめてみました。
クラスウィザードの使い方とかはわかっている人向け。自分用メモ。
こんなダイアログを目標に作っていきます。
動作仕様としてはこんな感じ
- Enter と Esc を無視したい
- [Start] をクリックしたら別スレッドで適当な処理をする。ここではプログレスバーが増えるだけ
- プログレスバーは10段階。1秒で1増えて、10秒で終了
- 処理をしている間は 閉じる[X] を禁止したい。[Start]も押せないようにしたい
- 処理をしている間は ぐるぐるカーソル表示
長い記事になりますが、ご容赦を。
2. ということで順番に作っていきます。
2-1. Enter と Esc を無視したい
Enter、 Esc でダイアログが閉じるのは仕様なのですけど、凝ったダイアログを作っていると無視したいことがあります。
以下に情報があります。
https://www.paveway.info/entry/2019/02/28/mfc_notclose
クラスウィザードから PreTranslateMessage をオーバーライドします。
コード。
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 とします。
ダイアログにプログレスバーを配置したら、コントロール変数を割り当てます。
ヘッダに以下、
CProgressCtrl m_Progress;
cpp に以下が付加されます。
void CThreadTestDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_PROGRESS_TEST, m_Progress); // <-これ
}
プログレスバーに対し、範囲とを現在位置(0)を設定します。
具体的には 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に以下。
CButton m_ButtonStart;
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] をクリックしたら~ の部分はイベントハンドラを登録します。
登録すると以下のようになります。
afx_msg void OnBnClickedButtonStart();
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
void EnableControl(BOOL b);
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);
}
}
2-4. 裏処理
スレッドを作りましょう。スレッド用のクラスは別に作ったのでそれを利用します。
しょうもないクラスですけど、排他処理だけはしっかりやりましょう。
// 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;
};
// 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. タイマを作る
m_uTimerID = SetTimer(ID, tim, NULL);
IDは作りたいタイマのID。タイマが複数あったら他と被らないようにします。実際に作ったタイマIDが(0以外の)戻り値で返されるので、以降はこれで管理します。timは間隔でミリ秒単位。精度は「動くだけマシ」な程度。最後の引数はタイマ処理をする関数ポインタで、NULLを与えるとデフォルトの OnTimer 関数が呼ばれるようになります。
1秒ごとに更新だから、半分の500ミリ秒で監視したらええんちゃうの。
スレッドもついでに作っちゃいましょう。
CThreadMain m_Thread;
static const UINT_PTR m_cuTimerID;
UINT_PTR m_uTimerID;
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と比較します。
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();
AfxMessageBox(_T("Complete!"), MB_ICONINFORMATION);
EnableControl(TRUE);
m_Progress.SetPos(0);
}
return;
}
CDialogEx::OnTimer(nIDEvent);
}
2-5-3. タイマを止める
StopTimer() の中身は KillTimer です。
void StopTimer();
void CThreadTestDlg::StopTimer()
{
if (m_uTimerID != 0) {
KillTimer(m_uTimerID);
m_uTimerID = 0;
}
}
2-6. カーソル
CWaitCursorだとマルチスレッドに対応できないです。
「処理をしている間はぐるぐるカーソルを表示」の情報はなかなか見つけにくいです。
答: WM_SETCURSOR イベントハンドラを作りましょう。
OnSetCursor で スレッドが動いていたら
- BeginWaitCursor()
- TRUE を返す
この2点です。
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は余計なので削りました。まとめてヘッダとソースを置きます。
// 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;
};
// 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. 最後に
この記事がお役に立てば幸いです。