◆レジューム(サスペンド/休止からの自動復帰)・・・・電源管理2 その2

[ ホーム ] [ 上へ ]

<プログラムの流れ>

前頁で実装しなければならない 内容がだいたい分かったのですが、マルチスレッドになり、 流れがよくわかならいかもしれないので流れを下図に示します。

プログラムの中心はレジュームを管理するクラス(CResume)です。上図では メインウィンドウはダイアログになっていますが、常駐タイプのアプリケーションにする 場合でしたら非表示ウィンドウになるなど、作成するアプリケーションのタイプによって いろいろ変わると思うのであまり気にしないでください。

メインウィンドウのメンバとしてCResumeのインスタンスを作り、 レジュームしたい時間とともにCResume::SetTimeをコールすると、もし、スレッドが起動して 以前のレジュームを待機していたら、それを終了させます。図では示されていませんが、 もしタイマが有効であればCloseHandleをコールしてハンドルを閉じます。
スレッドが終了したら、改めてスレッドを作成してSetWaitableTimerをコールした後、 待機関数(WaitForSingleObjectEx)によってタイマがシグナルになる(レジュームされる)、又は メインウィンスレッドからスレッド終了イベント(CEvent)が発行されてスレッドの終了を 要求されるまで待ち続けます。(スレッドが一時停止している)

スレッド側で待機している間にスタンバイ/休止状態にした(なった)あと、 タイマがシグナルになってレジュームされると、待機関数は制御を返し、SetWaitableTimerの 第四引数で指定したコールバック関数(TimerAPCProc)がスレッド側で実行されます。
コールバック関数のコール=レジュームが行われたということなので、メインスレッドへ メッセージを送信します。メッセージを受け取ったメインスレッド側は、SetThreadExecutionStateを コールしてアプリケーション(メインスレッド)の活動を認識させて再度のスリープ状態への移行を 防ぎます。

<実装>

では、上記の流れに従って実装していきます。 VS2005のファイル(F)→新規作成(N)→プロジェクト(P)でアプリケーションウィザードを起動してください。 ダイアログボックスで以下のように入力/選択してください。

(新しいプロジェクト)
プロジェクトの種類(P)   :  MFC
テンプレート(T)         :  MFCアプリケーション
プロジェクト名(N)       :  (任意)

その他はデフォルト。→ OK
→次へ

(アプリケーションの種類)
アプリケーションの種類  :  ダイアログベース
(ユーザーインターフェイス機能)
メインフレームのスタイル  :  システムメニューのみにチェック

→次へ

(高度な機能)
デフォルトのまま→次へ

完了をクリックしてウィザードを終わらせてください。

アプリケーションクラス(CxxxxApp)とダイアログクラス(CxxxDlg)のスケルトンが出来ていますが、 そのままにして、肝心のCResumeから実装していきます。
VS2005のプロジェクト(P)→クラスの追加(C)でダイアログが表示されるので次のC++クラスを作成 してください。

カテゴリ(C)     :      C++
テンプレート(T) :      C++クラス
→追加 
(汎用C++クラスウィザード)
クラス名(L)     :      CResume
仮想デストラクタ:      チェックをつける
→完了

Resume.h(ヘッダファイル)とResume.cpp(実装ファイル)ができているので 以下のコードを書いてください。

// Resume.h////////////////////////////////////////////////////////////
#pragma once

#include <afxmt.h>     //同期オブジェクトヘッダ

//レジューム完了通知メッセージ
#define     WM_NOTIFY_RESUME    (WM_USER + 10)


//レジューム管理クラス
class CResume
{
//メンバ関数/////////////////////////////////////////////
public:
    //コンストラクタ
    CResume();
    
    //デストラクタ
    virtual ~CResume();

    //レジューム時間セット
    BOOL SetTime(CTime resumetime, CWnd *pNotifyWnd);
    
    //レジュームCancel
    void Cancel();

protected:
    //ワーカースレッド制御関数 (待機可能タイマ作成&レジューム時間セット)
    static UINT __cdecl ThreadCtrlFunc(LPVOID pParam);

    //タイマ完了ルーチン (レジューム時間到達時ルーチン)
    static VOID CALLBACK TimerAPCProc(LPVOID lpArg,
                                      DWORD dwLowVal,
                                      DWORD dwHightVal);

//メンバ変数//////////////////////////////////////////////
protected:
    HWND            m_hNotifyWnd;     //レジューム完了を通知するウィンドウへのハンドル
    CWinThread      *m_pThread;       //セカンダリスレッドへのポインタ

    CTime           m_resumetime;     //レジューム時間(メインでのみSet)
    BOOL            m_bTmrSetting;    //待機可能タイマ設定結果(スレッドでのみSet)

    CEvent          m_evtTmrSetting;  //待機可能タイマ設定結果報告用イベント(自動イベント)
    CEvent          m_evtTerminate;   //スレッド終了用イベントオブジェクト(手動イベント)
};
Resume.cpp//////////////////////////////////////////////

#include "StdAfx.h"
#include "Resume.h"

//コンストラクタ
CResume::CResume() : m_hNotifyWnd(NULL)
                   , m_pThread(NULL)
                   , m_resumetime(0)
                   , m_bTmrSetting(FALSE)
                   , m_evtTmrSetting(FALSE, FALSE, _T("TmrSetting"))	//自動イベント
                   , m_evtTerminate(FALSE, TRUE, _T("ThrdTerminate"))	//手動イベント
{
}

//デストラクタ
CResume::~CResume()
{
    Cancel();
}

//レジュームCancel(スレッド終了 & スレッドオブジェクト(m_pThread)破壊
void CResume::Cancel()
{
    //スレッドオブジェクトが存在するとき
    if (m_pThread)
    {
        //スレッド終了確認
        m_evtTerminate.SetEvent();
        ::WaitForSingleObject(m_pThread->m_hThread, INFINITE);
        m_evtTerminate.ResetEvent();        //(A)

        //スレッドオブジェクト破壊
        delete      m_pThread;
        m_pThread = NULL;
    }
}

//レジューム時間セット
//引数  resumetime  レジューム予定時間
//      pNotifyWnd  レジューム完了を報告するウィンドウ
//戻値  レジューム予定がresumetimeでセットできたときTRUE
BOOL CResume::SetTime(CTime resumetime, CWnd *pNotifyWnd)
{
    //スレッドが起動中の時は終了 & スレッドオブジェクト(m_pThread)破壊
    Cancel();

    //レジューム完了通知先ウィンドウハンドル設定
    m_hNotifyWnd = pNotifyWnd->m_hWnd;
    if (! ::IsWindow(m_hNotifyWnd) )
        return FALSE;

    //レジューム時間設定
    m_resumetime = resumetime;

    //新規スレッド作成
    m_pThread = AfxBeginThread(CResume::ThreadCtrlFunc,
                               (LPVOID)this, 0, 0, CREATE_SUSPENDED, 0);
    if (! m_pThread)
        return FALSE;

    m_pThread->m_bAutoDelete = FALSE;
    m_pThread->ResumeThread();

    //待機可能タイマ作成結果がm_bTmrSettingに報告されるまで待機
    CSingleLock         slock(&m_evtTmrSetting, FALSE);
    slock.Lock(INFINITE);

    //待機可能タイマが失敗した時はレジューム予定時間を初期化
    if (! m_bTmrSetting)
        m_resumetime = 0;

    return m_bTmrSetting;
}

//ワーカースレッド制御関数(待機可能タイマ作成 & レジューム時間セット)
//引数	pParam	CResumeへのポインタ
/*static*/ UINT CResume::ThreadCtrlFunc(LPVOID pParam)
{
    CResume         *pResume = reinterpret_cast<CResume *>(pParam);
    HANDLE          hTimer   = NULL;        //待機可能タイマハンドル

    pResume->m_bTmrSetting = FALSE;
    do
    {
        //待機可能タイマ作成
        hTimer = ::CreateWaitableTimer(NULL, FALSE, _T("WTmr"));
        if (! hTimer)
            break;

        //レジュームを設定する時間が現時刻以前の場合は中止
        if ( pResume->m_resumetime <= CTime::GetCurrentTime() )
            break;

        //現時刻から指定時間までの時間を算出(100ns単位なので秒数に-10000000を掛ける)
        LARGE_INTEGER	li;
        CTimeSpan       span = pResume->m_resumetime - CTime::GetCurrentTime();
        li.QuadPart = span.GetTimeSpan() * -10000000;

        //待機可能タイマセット(レジュームセット)
        if (! ::SetWaitableTimer(hTimer, &li, 0, 
                                 CResume::TimerAPCProc, (LPVOID)pResume, TRUE)
           )
            break;

        pResume->m_bTmrSetting = TRUE;		
    } while(0);

    //メインスレッド(SetTime)に報告(メインスレッドでm_bTmrSettingを評価)
    pResume->m_evtTmrSetting.SetEvent();

    //レジューム時間が正常に設定できた場合は警告可能状態で待機する----------------
    //(m_evtTerminateがシグナル or 完了ルーチン(APC)がスレッドキューに置かれるまで)
    if (pResume->m_bTmrSetting)
        ::WaitForSingleObjectEx(pResume->m_evtTerminate, INFINITE, TRUE);   //(B)
   //-----------------------------------------------------------------------------

    //タイマを閉じる
    if (hTimer)
    {
        ::CloseHandle(hTimer);          hTimer = NULL;
    }

    AfxEndThread(0, FALSE);
    return 0;
}

//タイマ完了ルーチン(レジューム時間到達時)
//引数	lpArg       CResumeへのポインタ
//      dwLowVal    タイマシグナル時の時刻の下位値
//      dwHighVal   タイマシグナル時の時刻の上位値
/*static*/ VOID CResume::TimerAPCProc(LPVOID lpArg, DWORD dwLowVal, DWORD dwHightVal)
{
    CResume     *pResume = reinterpret_cast<CResume *>(lpArg);

    //レジューム完了を報告
    ::PostMessage(pResume->m_hNotifyWnd, WM_NOTIFY_RESUME, 0, 0);
}

上記のコードで少し注意したい点の1つ目は、スレッドの終了に関わる CResume::m_evtTerminate同期オブジェクト(CEvent)です。これは、SetWaitableTimerを実行した結果を セカンダリスレッドからメインに通知するイベント(m_evtTmrSetting)と異なり手動イベントです。 つまり、イベントがシグナルになってスレッドが解放されても自動的に非シグナルにならないで そのままシグナルのままです。そのため(A)でResetEventを呼んでいます。自動イベントに しない理由は次のとおりです。
CResume::SetTimeを呼ぶ際には、一旦前に設定したレジューム時間をクリアするために CResume::Cancelを呼んでスレッドの終了 & スレッドオブジェクト(CResume::m_pThrea)の破壊を 実行しています。もし、前のレジューム設定で現在の時刻より前の時間を設定しようとして SetWaitableTimerが失敗した場合、スレッド制御関数は(B)で待機することなくスレッドは 終了してしまいます。(m_pThreadのインスタンスはあります)
この後、再びCResume::SetTimeを実行した場合、もし自動イベントだとしたら CResume::Cancelを呼んだときm_evtTerminateはシグナルに設定されます。しかし、スレッドは 終了してしまっているので、(B)の待機関数が解放されることはなく、解放するスレッドは ないのでそのままシグナルのままになっています。
この状態でCResume::SetTimeの残りを実行して(B)に制御がきたらどうでしょう?レジュームの 設定はうまくいっているのに待機することなくスレッドは終了してしまいます。これを回避するために 手動で(A)の2行下で手動でResetEventを実行しています。

もう一点はコールバック関数の使用についてです。レジュームのことだけを 考えれば、わざわざコールバック関数(CResume::TimerAPCProc)を使わなくても待機関数に WaitForMultipleObjectsExでタイマオブジェクトも待機するようにすればいいはずです。
これに関してはSetWaitableTimerの機能の例を書くためというのが大きな理由なので、 簡素化したい方は修正してください。

レジュームに関する中心部分は上記までなので、あとは適当にCResumeを 使ってください、で済ませてもいいのですが、使い方の例をひとつだけ挙げておきます。
アプリケーションウィザードでダイアログクラス(CxxxDlg)のスケルトンができているので、 これに時間を入力してレジュームを実行するアプリケーションを作ってみます。

VS2005のリソースビューを表示してDialogの項目からIDD_xxxxx_DIALOGを ダブルクリックしてリソースエディタでダイアログを表示してください。表示したら、 以下のように各タイプのコントロールを貼り付けて必要なIDを設定してください。

番号 コントロールタイプ ID
[1] Edit Control IDC_YEAR
[2] Edit Control IDC_MONTH
[3] Edit Control IDC_DAY
[4] Edit Control IDC_HOUR
[5] Edit Control IDC_MINUTE
[6] Edit Control IDC_SECOND
[7] Static Text IDC_STC_RESUME
[8] Button IDC_BTN_SET
[9] Button IDC_BTN_CANCEL
[10] Button IDOK

設定できたら保存してCResumeをつかってCxxxDlgを実装します。ヘッダファイル (xxxDlg.h)、実装ファイル(xxxDlg.cpp)に以下の加筆をしてください。

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

#pragma once

#include "Resume.h"

// CxxxDlg ダイアログ
class CxxxDlg : public CDialog
{
//  ・
//  ・
//  ・

//メンバ変数///////////////////////////////////////////////////////////////
protected:
    CResume     m_Resume;                   //レジューム管理クラス

//メンバ関数///////////////////////////////////////////////////////////////
public:
    afx_msg void OnBnClickedBtnSet();       //レジューム設定ボタン
    afx_msg void OnBnClickedBtnCancel();    //レジューム設定無効

protected:
    afx_msg LRESULT OnNotifyResume(WPARAM wParam, //レジューム完了通知Message Handler
                                   LPARAM lParam);

    DECLARE_MESSAGE_MAP()
};
// xxxDlg.cpp : 実装ファイル
//

#include "stdafx.h"
#include "xxx.h"
#include "xxxDlg.h"

//  ・
//  ・

// CxxxDlg ダイアログ


//  ・
//  ・
//  ・

BEGIN_MESSAGE_MAP(CtestWaitableTmrDlg, CDialog)
//  ・
//  ・
    ON_BN_CLICKED(IDC_BTN_SET, &CxxxDlg::OnBnClickedBtnSet)
    ON_BN_CLICKED(IDC_BTN_CANCEL, &CxxxDlg::OnBnClickedBtnCancel)

    ON_MESSAGE(WM_NOTIFY_RESUME, &CxxxDlg::OnNotifyResume)
END_MESSAGE_MAP()

//  ・
//  ・
//  ・

//レジューム時間設定ボタンクリック
void CxxxDlg::OnBnClickedBtnSet()
{
    int     nID[]   = {IDC_YEAR, IDC_MONTH,  IDC_DAY,        //各エディットボックスID
                       IDC_HOUR, IDC_MINUTE, IDC_SECOND
                      };
    int     nData[ sizeof nID / sizeof nID[0] ];             //各エディットボックスの値
    CTime   cur     = CTime::GetCurrentTime();               //現時刻
    BOOL    bErr    = FALSE;

    //各エディットボックスの入力値の検査 & 取得
    for (int i=0; i < (sizeof nID / sizeof nID[0]); i++)
    {
        CString         str;

        //入力値をnData[]に取得
        GetDlgItem( nID[i] )->GetWindowText(str);
        if (str.IsEmpty())      {bErr = TRUE;       break;}

        //数値変換
        errno = 0;
        TCHAR       *pszStop = NULL;
        nData[i] = _tcstol(str, &pszStop, 10);
        if (errno || *pszStop)  {bErr = TRUE;       break;}

        //入力値チェック
        switch ( nID[i] )
        {
        case IDC_YEAR:     if (nData[i] < cur.GetYear())      {bErr = TRUE;}     break;
        case IDC_MONTH:    if (nData[i] < 1 || nData[i] > 12) {bErr = TRUE;}     break;
        case IDC_DAY:      if (nData[i] < 1 || nData[i] > 31) {bErr = TRUE;}     break;
        case IDC_HOUR:     if (nData[i] < 0 || nData[i] > 23) {bErr = TRUE;}     break;
        case IDC_MINUTE:   if (nData[i] < 0 || nData[i] > 59) {bErr = TRUE;}     break; 
        case IDC_SECOND:   if (nData[i] < 0 || nData[i] > 59) {bErr = TRUE;}     break;
        default:            ;
        }
    }
    if (bErr)
    {
        ::AfxMessageBox(_T("入力エラー"));      return;
    }

    //以前の設定を無効
    OnBnClickedBtnCancel();

    //レジューム時間設定
    ASSERT(sizeof nData / sizeof nData[0] == 6);
    CTime       t(nData[0], nData[1], nData[2], nData[3], nData[4], nData[5]);
    CString     strResult;

    if (m_Resume.SetTime(t, this) )
        strResult.Format(_T("%d/%d/%d   %d:%d:%d"), nData[0], nData[1], nData[2],
                                                    nData[3], nData[4], nData[5]);

    //結果表示
    GetDlgItem(IDC_STC_RESUME)->SetWindowText(strResult);
}

//レジューム設定無効
void CxxxDlg::OnBnClickedBtnCancel()
{
    ::SetThreadExecutionState(ES_CONTINUOUS);
    m_Resume.Cancel();
    
    GetDlgItem(IDC_STC_RESUME)->SetWindowText(_T(""));
}

//レジューム完了通知Message Handler
LRESULT CxxxDlg::OnNotifyResume(WPARAM wParam, LPARAM lParam)
{
    ::SetThreadExecutionState(ES_SYSTEM_REQUIRED | ES_CONTINUOUS);

    ::MessageBeep(MB_ICONASTERISK);
    TRACE(_T("ここで任意の作業をしてください"));
    
    return 0;
}

以上で出来上がりなのでビルドして、レジューム時間を入力後、 レジューム時間設定ボタンを押下してください。設定がうまくいったら、ダイアログを 終了させないでサスペンド/休止してください。時間が来ると復帰するはずです。

ダイアログクラスへの実装はCResumeを使うだけなので特にコメントはないですが、 一番重要な点はSetThreadExecutionStateをCxxxDlg::OnNotifyResumeで呼んでいることです。 ES_SYSTEM_REQUIRED | ES_CONTINUOUSを引数に指定することによりシステムにこのスレッドが アクティブ(作業している)ということを知らせて再度スリープ状態になることを回避しています。

CxxxDlg::OnBnClickedBtnSetのほとんどは入力値の検査ですが、たぶんDDX/DDVを 使ったほうが楽でしょう。DDX/DDVを使うとCxxxDlg::DoDataExchangeなど加筆箇所が分散して 説明しづらいので今回は自分で入力値のチェックをしました。
レジュームの完了がCxxxDlg::OnNotifyResumeで知らされますが、コードではビープ音を鳴らして いるだけですが、実際には、例えば夜間の無人状態でレジュームさせた際に任意の作業を行うこと になるかと思います。

上記コードは簡単な例として掲載しましたが、応用編?として以下のほんの少しの 修正で簡易的な目覚まし時計が出来上がります。
1.SetWaitableTimerの第三引数を変更して周期タイマにする。
2.スレッド待機関数(WaitForSingleObjectEx)は現状、タイマのシグナルまたはスレッド終了 イベント(m_evtTerminate)のシグナルで制御を返し終了しますが、ここを半永久ループにして m_evtTerminateのみループを抜けるようにする。
興味があればやってみてください。

参考文献: System Wake-up Events
Using Waitable Timers with an Ansynchronous Procedure Call
電源コントロール