◆マルチスレッド - VC++ 2005 MFC 〜 ユーザーインターフェイススレッド その1

[ ホーム ] [ 上へ ] [ マルチスレッド VC++ 2005 MFC 〜 ユーザインターフェイススレッド その2 ]

・スレッド作成の実際(ユーザインターフェイススレッド)- WindowsXP + VC++ 2005

ワーカースレッドではセカンダリスレッド内でウィンドウは作成されないので、 見た目に複数のスレッドが起動していることがわかりづらいです (もちろん動作をみれば確認できると思います)。今回は簡易な ストップウォッチをつくってユーザインターフェイスの動作を確認してみます。

ダイアログベースのプロジェクトを作成するので、 プライマリスレッド側はCDialogの派生クラスがメインウィンドウになり、こちらでストップウォッチの スタート/ストップを担当します。セカンダリスレッドではウィンドウを表示して 時間の経過を表示します。 調整はできますが、10ミリ秒単位でタイマを実行して経過時間を表示します。10msのタイマの精度に 関してはちょっと疑問がありますが本題とは直接関係ないので無視します。短い時間間隔でSetTimerの コールバック関数が呼ばれ続けるので、別スレッドで動作させる意味は十分あります。では作ってみます。

メニューのファイル(F)→新規作成(N)→プロジェクト(P)でアプリケーションウィザードから

プロジェクトの種類(P) : Visual C++
テンプレート      : MFCアプリケーション
プロジェクト名     : StopWatch(任意)
ソリューション名        :  UIThread(任意) 
→OK
 
→次へ
アプリケーションの種類 : ダイアログベース(D)
(他はデフォルト)
→次へ
 
メインフレームのスタイル : システムメニュー(S)のみチェック
→次へ
 
高度な機能 : (デフォルト)
→次へ
→完了

次にダイアログリソースを作成します。ツールバーの 表示(V)→その他のウィンドウ(E)→リソースビュー(R)でリソースビューを表示してダイアログを 以下のように作成してください。

各コントロールを右クリックしてプロパティ(R)を実行して プロパティシートを表示後、以下のようにプロパティ・管理イベントを設定してください。

  コントロールタイプ プロパティ・イベント
ボタン IDプロパティ IDC_BTN_START
Captionプロパティ Start
BN_CLICKEDイベント <追加> OnBnClickedBtnStart
ボタン IDプロパティ IDC_BTN_STOP
Captionプロパティ Stop
BN_CLICKEDイベント <追加> OnBnClickedBtnStop
ボタン IDプロパティ IDC_BTN_CLEAR
Captionプロパティ Clear
BN_CLICKEDイベント <追加> OnBnClickedBtnClear

上記の各ボタンのクリック時の実装をするのですが、ストップウォッチの経過時間の 取得・表示機能はセカンダリスレッドが担当するので、先にセカンダリスレッド (CWinThreadの派生クラス)と、セカンダリスレッドで稼動する表示ウィンドウ(CWndの派生クラス)を 実装します。それぞれの名前をCSecThread、CDspWndとします。 まず、CDspWndクラスの作成&実装から。

<セカンダリスレッドで表示するウィンドウ実装 - CDspWnd(CWnd派生クラス)>

ツールバーのプロジェクト(P)→クラスの追加(C)で「クラスの追加」 ダイアログで以下のように設定してください。

カテゴリ(C)     :MFC
テンプレート(T) :MFCクラス
→追加(A)
 
クラスウィザードで次のように設定してください。
クラス名(L)  :CDspWnd
基本クラス(B):CWnd (デフォルト)
→完了

DspWnd.hとDspWnd.cppにCDspWndクラスのヘッダファイル・実装ファイルのスケルトンが できているので、次のようにコードを記述してください。

(DspWnd.h)

//DspWnd.h : ヘッダファイル
//
#pragma once

//----------------------------------------------------------------------------------
#define     WM_TERMINATEAPP     (WM_USER + 10)     //アプリケーション終了メッセージ
#define     TIMER_SPAN          10                 //ストップウォッチ間隔(ミリ秒単位)
//----------------------------------------------------------------------------------

// CDspWnd - 経過時間表示ウィンドウ
class CDspWnd : public CWnd
{
    DECLARE_DYNAMIC(CDspWnd)

public:
    CDspWnd();                                      //コンストラクタ
    virtual ~CDspWnd();                             //デストラクタ

//メンバ変数////////////////////////////////////////////////////////////////////////
protected:
    CPoint          m_point;                        //クライアント領域中心位置
    DWORD           m_dwStart;                      //スタート時間(ミリ秒単位)
    DWORD           m_dwCurrent;                    //現在時間(ミリ秒)
    BOOL            m_bBusy;                        //ストップウォッチ開始フラグ

//メンバ関数////////////////////////////////////////////////////////////////////////
protected:
    virtual void PostNcDestroy();                   //C++オブジェクト自動削除

public:
    BOOL Create();                                  //Window構築

    afx_msg void OnDestroy();                       //Window破壊時
    afx_msg void OnTimer(UINT_PTR nIDEvent);        //タイマ
    afx_msg void OnPaint();                         //クライアント領域描画

    BOOL Start(UINT nElapse = TIMER_SPAN);          //ストップウォッチ開始
    BOOL Stop();                                    //ストップウォッチ停止
    BOOL Clear();                                   //ストップウォッチデータクリア

protected:
    DECLARE_MESSAGE_MAP()
};

(DspWnd.cpp)

// DspWnd.cpp : 実装ファイル
//
#include "stdafx.h"
#include "StopWatch.h"
#include "DspWnd.h"

#define     TIMER_ID      1 //タイマID

// CDspWnd

IMPLEMENT_DYNAMIC(CDspWnd, CWnd)

//コンストラクタ
CDspWnd::CDspWnd()
: m_point(0, 0)
, m_dwStart(0)
, m_dwCurrent(0)
, m_bBusy(FALSE)
{
}

//デストラクタ
CDspWnd::~CDspWnd()
{
}

BEGIN_MESSAGE_MAP(CDspWnd, CWnd)
    ON_WM_DESTROY()
    ON_WM_TIMER()
    ON_WM_PAINT()
END_MESSAGE_MAP()

// CDspWnd メッセージハンドラ

//Window構築
BOOL CDspWnd::Create()
{
    CRect       rect;

    //CDspWndの表示位置取得
    theApp.m_pMainWnd->GetWindowRect(&rect);
    rect.OffsetRect(0, -1 * rect.Height());

    //Window構築
    if (! CWnd::CreateEx(0,
                         AfxRegisterWndClass(CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW,
                                             0,
                                             reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1),
                                             0),
                        _T("Stop Watch"),
                        WS_OVERLAPPED,
                        rect,
                        NULL,
                        0)
    )
        return FALSE;

    //クライアント領域更新
    UpdateWindow();

    //Window表示
    ShowWindow(SW_SHOW);

    //秒数表示位置取得
    GetClientRect(&rect);
    m_point = rect.CenterPoint() - CPoint(10, 5);

    return TRUE;
}

void CDspWnd::OnDestroy()
{
    CWnd::OnDestroy();

    //StopWatchDlgにアプリケーション終了を通知。
    theApp.m_pMainWnd->PostMessage(WM_TERMINATEAPP, 0, 0);
}

//C++オブジェクト自動破壊
void CDspWnd::PostNcDestroy()
{
    delete this;
}

//タイマ
void CDspWnd::OnTimer(UINT_PTR nIDEvent)
{
    if (nIDEvent == TIMER_ID)
    {
        m_dwCurrent = ::GetTickCount();
        Invalidate();
    }
}

//クライアント領域描画
void CDspWnd::OnPaint()
{
    CPaintDC dc(this); // device context for painting
    // TODO: ここにメッセージハンドラコードを追加します。
    // 描画メッセージでCWnd::OnPaint() を呼び出さないでください。
    CString            strMsg;

    strMsg.Format(_T("%d.%02d"), (m_dwCurrent - m_dwStart) / 1000,
                                 (m_dwCurrent - m_dwStart) % 1000 / 10);

    dc.TextOutW(m_point.x, m_point.y, strMsg);
}

//ストップウォッチ開始
BOOL CDspWnd::Start(UINT nElapse /*=TIMER_SPAN*/)
{
    BOOL     bRet = FALSE;

    if (! m_bBusy && ! m_dwStart && ! m_dwCurrent)
    {
        m_dwStart = ::GetTickCount();
        bRet = SetTimer(TIMER_ID, nElapse, NULL) ? TRUE : FALSE;

        m_bBusy = TRUE;
    }
    return bRet;
}

//ストップウォッチ停止
BOOL CDspWnd::Stop()
{
    BOOL     bRet = KillTimer(TIMER_ID);
    
    m_bBusy = FALSE;

    return bRet;
}

//ストップウォッチデータクリア
BOOL CDspWnd::Clear()
{
    BOOL     bRet = FALSE;

    if (! m_bBusy)
    {
        m_dwStart = 0;         m_dwCurrent = 0;

        Invalidate();

        bRet = TRUE;
    }
    return bRet;
}

このウインドウは、このあと作成するセカンダリスレッドの メインウィンドウに設定することになります。各メンバの機能は次の通りです。

m_point ストップウォッチの経過時間を表示する位置を設定するためのCPoint型変数。Create関数内で位置の設定をして、OnPaint関数内で使用しています。
m_dwStart ストップウォッチ開始時刻。Start関数で現在時刻に設定され、OnTimerコールバック関数でm_dwCurrentに取得される時刻との差を経過時間として表示しています。
m_dwCurrent ストップウォッチ開始後(タイマ開始後)、OnTimer関数で現在の時刻を取得してStart関数で設定したm_dwStartとの差を表示します。
m_bBusy ストップウォッチの実行中フラグ。Start関数でTRUEに設定されて、Stop関数でFALSEに設定される
PostNcDestroy関数 CDspWndウィンドウが閉じられて最後に呼ばれる関数で、この後に記述する、セカンダリスレッドであるCSecThreadのInitInstanceで作成されたC++オブジェクトを自動的に破壊します。
Create関数 CWnd::CreateExを呼んでWindowを構築し、CStopWatchDlgダイアログの上部に表示します。加えて、ストップウォッチの表示位置を計算しています。
OnDestroy関数 ユーザがCDspWndを閉じるときに、APIのDestroyWindow関数が呼ばれたのちにフレームワークに呼ばれる関数。この関数内で、ユーザ定義のWM_TERMINATEAPPメッセージをプリマリスレッドのCStopWatchDlgに送り、このウィンドウが破壊され、セカンダリスレッドが終了するのを待った後、メインのダイアログが終了します。
OnTimer関数 Start関数でSetTimerを呼び出してタイマを実行した後に呼ばれるコールバック関数。現在の時刻をm_dwCurrentに取得してクライアント領域を再描画(OnPaint)します。
OnPaint関数 OnTimerでクライアント領域の再描画要求されると、この関数が呼ばれストップウォッチを開始した時刻(m_dwStart)とタイマ時に取得した時刻(m_dwCurrent)の差を計算して経過時間を表示します。
Start関数 ストップウォッチが停止していること、以前のデータがクリアされていることを確認してストップウォッチを開始します。
Stop関数 タイマを停止してストップウォッチを止め、ストップウォッチ稼動フラグ(m_bBusy)をおろします。
Clear関数 ストップウォッチが停止していることを確認したうえで、過去の表示データ(m_dwCurrentとm_dwStart)をクリアして再描画します。

<セカンダリスレッド実装>

続いてAfxBeginThreadに渡すCWinTheadの派生クラスを作成&実装します。

ツールバーのプロジェクト(P)→クラスの追加(C)で 「クラスの追加」ダイアログで以下のように設定してください。

カテゴリ(C)     :MFC
テンプレート(T) :MFCクラス
→追加(A)
 
クラスウィザードで次のように設定してください。
クラス名(L)     :CSecThread
基本クラス(B)   :CWinThread
→完了

SecThread.hとSecThread.cppにCSecThreadクラスのヘッダファイル・実装ファイルの スケルトンができているので、次のようにコードを記述してください。

(SecThread.h)

//SecThread.h :ヘッダファイル

#pragma once

// CSecThread
class CSecThread : public CWinThread
{
    DECLARE_DYNCREATE(CSecThread)

protected:
    CSecThread(); // 動的生成で使用されるprotected コンストラクタ

public:
    virtual ~CSecThread();

public:
    virtual BOOL InitInstance();
    virtual int ExitInstance();

protected:
    DECLARE_MESSAGE_MAP()
};

(SecThread.cpp)

// SecThread.cpp : 実装ファイル
//
#include "stdafx.h"
#include "StopWatch.h"
#include "SecThread.h"
#include "DspWnd.h"

// CSecThread

IMPLEMENT_DYNCREATE(CSecThread, CWinThread)

//コンストラクタ(protected)
CSecThread::CSecThread()
{
}

//デストラクタ(public)
CSecThread::~CSecThread()
{
}

BOOL CSecThread::InitInstance()
{
    CDspWnd *pDsp = new CDspWnd;

    m_pMainWnd = pDsp;              //メインウィンドウに設定 (A)

    return pDsp->Create();
}

int CSecThread::ExitInstance()
{
    // TODO: スレッドごとの後処理をここで実行します。
    return CWinThread::ExitInstance();
}

BEGIN_MESSAGE_MAP(CSecThread, CWinThread)
END_MESSAGE_MAP()

AfxBeginThreadでスレッド作成後、 _AfxThreadEntry関数が呼ばれて上記の CWinThread::InitInstanceでCDspWndウィンドウが構築 & 表示されます。

(A)でセカンダリスレッドのメインウィンドウをCDspWndに設定しています。 スレッドのメインウィンドウ(m_pMainWnd)に設定されたウインドウが閉じられると、 APIのPostQuitMessageが呼ばれてWM_QUITがメッセージキューに送られ CWinThread::RunでGetMessageがFALSEが返して CWinThread::ExitInstanceが呼ばれた後、無限ループから抜けます。その後、 _AfxThreadEntryでAfxEndThreadで スレッドが終了します。
つまり、CWinThread::m_pMainWndに設定されたウインドウ (CDspWnd)が閉じられると、自動的にそのスレッドが 閉じられるということです。現段階ではこの点だけを覚えておいてください。

以上がセカンダリスレッド部分の実装です。