タイマー関数

一定時間間隔で測定機器からデータをパーソナルコンピュータに取り入れたい場合があります。ここで紹介するのは10msecの精度を持つマルチメディア タイマー関数(timeSetEvent関数)です。WindowsというOS下では、いろいろなプログラムが走っていて、OSはそれぞれのプログラムに実行時間を割り当てます。したがって、自分のプログラムのある関数が実行開始できるまでには、どれくらい時間が必要であるか分かりませんし、また、その必要時間も他に走っているプログラムの状況の変化とともに時々刻々と変わります。タイマー関数も例外ではなく、最悪10msec程度はOSに待たされる可能性があります。結局、測定値が10msec程度の間は変わらない用途で良ければ以下を参照ください。そして、これ以上の高精度を要求する場合は、外部にマイクロ コンピュータをともなったインターフェースの導入をお勧めします。


timeSetEvent関数

MMRESULT timeSetEvent(UINT uDelay, UINT uResolution, LPTIMECALLBACK lpTimeProc, DWORD dwUser, UINT fuEvent);

戻り値はMMRESULT型でタイマーのID(複数のタイマーを起動させたときに、それらを区別するための識別子identifier)です。また引数は5つあります。uDelayはmsec単位での遅延時間です。1秒ごとにデータを得たい場合には1000とします。uResolutionはmsec単位での精度です。1msecで良いでしょう(運がよければ1msecの精度を得れられるということ。1msecに設定してもWindowsOSに待たされる可能性があるので、常に1msecの精度が得られるということではありません)。lpTimeProcは時間が来たときに呼ぼうとするコールバック関数名です。dwUserはコールバック関数に渡すデータです。最後のfuEventはTIME_PERIODICに設定するとuDelayで指定される時間ごとにlpTimeProcで指定される関数が呼ばれます。

たとえば、1秒ごとに1回Ringというコールバック関数を呼びたいときには

MMRESULT uTimerID=timeSetEvent(1000,0,Ring,0, TIME_PERIODIC);

とします。


使用例

メニューで”開始”を選択すると、1秒ごとにビープ音がなるプログラムを作ります。まず、ウィンドウ事始めの普通のウィンドウで紹介した要領にて、プロジェクト名を"timer"としてプロジェクトを作りましょう。実行して普通のウィンドウが現れることを確認します。次に、ウィンドウのカスタマイズで説明した要領にてメニューを作くってください。作成過程で、メニューアイテム プロパティというダイアログ ボックスのところで、ポップアップについているチェックをクリックで外し、ID欄にID_START(半角で入力)、キャプション欄に開始と書き込んでください。また、ハンドラー関数を追加するときに、その名前はOnStartで良いかどうか聞かれますのでそのままOKとしてください。ファイルtimer.cppの最後に、void CMainFrame::OnStart() という関数が追加されていることを確認するとともに、プログラムを実行して、メニューが表示されていることも確認してください。

確認後

void CALLBACK Ring(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)
{
	Beep(1000,100);
}

void CMainFrame::OnStart() 
{
	MMRESULT uTimerID=timeSetEvent(1000,0,Ring,0, TIME_PERIODIC);
}
のように、Ringというコールバック関数をOnStart() 関数の(必ず)前に挿入して、またOnStart()関数の中にtimeSetEvent関数を書きます。これでRing関数は1秒間に1回呼ばれるのですが、そのRing関数ではBeep関数を呼ぶことにより1000Hzで100msec間ビープ音がなるようにしています。さらにtimer.h(File ViewでのHeader Filesの下にあります)の上方で
#include "resource.h"
#include "mmsystem.h"
#pragma comment(lib, "winmm.lib")
のように、#include "resource.h"の下に#include "mmsystem.h"と#pragma comment(lib, "winmm.lib")の2行を付け加えます。前者がtimeSetEvent関数の定義で、後者がtimeSetEvent関数の本体が入っているライブラリを組み込む(リンク:linkする)ためのものですが、timeSetEvent関数を使うためのおまじないと思ってください。以上で完成ですのでプログラムを実行してください。なお、できあがったプログラムはtimer.zipです。


オブジェクト指向

以上で話は終わりであるといいたいのですが、コールバック関数RingにはCMainFrame::OnStart() 関数とは違い、CMainFrame::がついていません。つまりRingはCMainFrameクラスのメンバー関数ではないのです。グローバル(大域)関数といって、プログラムのどこからでも呼び出せるのです。必要の無いところから呼び出せるということは、間違って呼んでしまうという危険性を伴います。必要な所からしか呼び出せないようにするというオブジェクト指向の考え方に沿えば、クラスのメンバー関数を、例えばCMainFrame::Ringとして、これをコールバック関数のように扱いたいところですが、問題が2つあります。

1.一般的に、クラスのメンバー関数は、暗黙のうちに”this”ポインター(自分のクラスの実体自身を指し示すポインター)をその引数として持つので(見た目には分かりません)、コールバック関数("this"とは全く無縁である)とはなりません。

実際に確認してみよう。まず、メンバー関数を追加します。 ワークスペースの下方に3つのタブのうちClass Viewをクリックします。現れるtimerクラスのツリーをすべて開くと右図のように表示されるはずです。ここで、CMainFrameと書かれた所を右クリックすると、下の左図のようなポップアップメニューが現れるので、そこで、メンバー関数の追加をクリックします。すると、下の右図のようなメンバー関数の追加ダイアログが現れますので、関数の型をvoid CALLBACKとし、関数の宣言をRing(UINT, UINT, DWORD, DWORD, DWORD)とし、アクセス制御に対しProtect(メンバー関数は通常Protectにします。現在のクラスから派生したクラスを作る場合、その派生クラスからも現在のクラスのメンバー関数を呼べるようにしたいからです。)を選択してください。そして、OKボタンを押します。






timer.cppの最後にvoid CALLBACK CMainFrame::Ring(UINT, UINT, DWORD, DWORD,DWORD)なる関数が、以下のように書き込まれているはずです。

void CALLBACK Ring(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)
{
	Beep(1000,100);
}

void CMainFrame::OnStart() 
{
	MMRESULT uTimerID=timeSetEvent(1000,0,Ring,0, TIME_PERIODIC);
}

void CALLBACK CMainFrame::Ring(UINT, UINT, DWORD, DWORD,DWORD)
{

}
ここで、CALLBACK Ring関数を削除し(Ring関数は1つでよいので)、CALLBACK CMainFrame::Ring関数の引数を(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)とし、さらにこの関数のなかにBeep(1000,100);を加えてつぎのようにします。
void CMainFrame::OnStart() 
{
	MMRESULT uTimerID=timeSetEvent(1000,0,Ring,0, TIME_PERIODIC);
}

void CALLBACK CMainFrame::Ring(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)
{
	Beep(1000,100);
}

ここで、プログラムを実行してみてください。timeSetEvent関数の第3引数のところでエラーがでるはずです。この第3引数はコールバック関数("this"とは全く無縁である)ですが、クラスのメンバー関数CMainFrame::Ringは、暗黙のうちに”this”ポインターをその引数として持って呼び出す必要があるので、問題が生じます。問題の解決にはクラスのメンバー関数でも”this”ポインターと関係なくしてしまえば良いことになります。それは、クラスのメンバー関数を"static"なものとします。具体的には、timer.h(File ViewでのHeader Filesの下にあります)の

protected:
	void CALLBACK Ring(UINT, UINT, DWORD, DWORD, DWORD);
のところを
protected:
	static void CALLBACK Ring(UINT, UINT, DWORD, DWORD, DWORD);
とします。ここでプログラムを実行してください。問題なく実行できるはずです。2つ目の問題に移ります。

2.staticなメンバー関数は"this"ポインターを持っていないので、この関数から、暗に"this"ポインターを渡さなければいけないクラスの他のメンバー関数の呼び出しは原理的に不可能です。

確かめてみます。timer.cに戻り、 Beep(1000,100);のすぐ下に、強制的に再描画する関数Invalidate(TRUE);を

void CALLBACK CMainFrame::Ring(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)
{
	Beep(1000,100);
	Invalidate(TRUE);
}
のように付け加えて実行してみてください。Invalidate関数はメンバー関数なので、エラーがでるはずです。"this"ポインターを暗に渡していないことから発生する問題です。この問題は、staticなメンバー関数が"this"ポインターの情報を、何らかの方法で獲得すれば解決するはずです。いろいろな方法があますが、ここでは、その1つを示します。 timeSetEvent(1000,0,Ring,0, TIME_PERIODIC);関数の第4引数がRing関数の第3引数dwUserに値を渡すことを利用して、そこで"this"ポインターを渡します。これを利用すればInvalidate関数のようなメンバー関数を呼び出せるようになります。
void CMainFrame::OnStart() 
{
	MMRESULT uTimerID=timeSetEvent(1000,0,Ring,reinterpret_cast<DWORD>(this), TIME_PERIODIC);
}

void CALLBACK CMainFrame::Ring(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)
{
	Beep(1000,100);
	(reinterpret_cast<CMainFrame*>(dwUser))->Invalidate(TRUE);
}
ごちゃごちゃ書いてありますが、"this"を渡しているという雰囲気だけをつかんでください。これでプログラムを実行することができるようになったので確かめてください。

以上の様にMainFrame::Ring関数のなかから、クラスのメンバー関数を呼び出すためには、(reinterpret_cast<CMainFrame*>(dwUser))->の後にメンバー関数を書けば良いのですが、いくつものメンバー関数を呼び出す場合、面倒になります。そこで、新しいメンバー関数を作り、それをたとえば、void CMainFrame:: RealCallBack(void)とし、これをRing関数から(reinterpret_cast<CMainFrame*>(dwUser))->RealCallBack();のように呼び出すことにすれば便利になります。RealCallBack関数はメンバー関数ですから、この中からは他のメンバー関数を自由に呼び出すことができます。具体的に作業してみましょう。

まず、以前行ったようにメンバー関数を追加します。メンバー関数の追加ダイアログで、関数の型をvoidとし、関数の宣言をRealCallBack(void)とし、アクセス制限に対しProtectを選択してください。そして、OKボタンを押します。ファイルtimer.cppの最後に、void CMainFrame::RealCallBack()という関数が追加されていることを確認するとともに、つぎのようにプログラムを書き換えてくだい。

 CALLBACK CMainFrame::Ring(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1,DWORD dw2)
{
	(reinterpret_cast<CMainFrame*>(dwUser))->RealCallBack();
}

void CMainFrame::RealCallBack()
{
	Beep(1000,100);
	Invalidate(TRUE);
}

実行してみてください。RealCallBack()の中から他のメンバー関数を簡単に呼び出すことができます。なお、できあがったプログラムはtimer1.zipです。実際の測定を行うプログラミング、そして必要であれば描画はRealCallBack()関数の中で行います。


タイマーの停止

タイマーを起動しましたが、タイマーを止める方法を説明していません。プログラムを終わったときにタイマーを止めておくのが礼儀です。礼儀というのは、止めずにそのままでもプログラムが終わったときにOSが自動的に止めますので、問題は起こりませんが、一応止めておくということです。またプログラムの終わりでない時にタイマーを止めたい場合があるので、タイマーを止める方法をここで紹介します。

まず、MMRESULT uTimerIDなる変数をクラスのメンバー変数にして、メンバー関数CMainFrame::OnStart()以外のメンバー関数からでも、このメンバー変数を参照できるようにします。timer.hを開いて、クラスの定義class CMainFrame : public CFrameWndのなかで、一番最後に

	//{{AFX_MSG(CMainFrame)
	afx_msg void OnPaint();
	afx_msg void OnStart();
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
private:
	MMRESULT uTimerID;
};
のように、private:とMMRESULT uTimerID;の2行を追加します(メンバー変数は原則としてprivate:にすることは、ウィンドウのカスタマイズ(customize)ーステータス バーの追加の所で説明しました)。これでメンバー変数が定義されたので、クラスのなか(具体的にはメンバー関数)からだけ自由にuTimerIDを使用できます。一方、void CMainFrame::OnStart()関数の中の定数uTimerIDの定義はいらなくなるので、次のようにMMRESULTを消します。

void CMainFrame::OnStart()
{
	uTimerID=timeSetEvent(1000,0,Ring,reinterpret_cast<DWORD>(this), TIME_PERIODIC);
}

次に、ウィンドウを閉じる時に、その前に呼ばれる関数(ディストラクタと呼ばれる)CMainFrame::~CMainFrame()の中に、タイマーを止めるためのtimeKillEvent関数を次のように

CMainFrame::~CMainFrame()
{
	timeKillEvent(uTimerID);
}
とします。これでウィンドウを閉じる時にタイマーをきちんと止めることができます。実は、このディストラクタのなかで、uTimerIDを使用できるように、uTimerIDをメンバー変数として定義したのです。あるメンバー関数の中で定義された変数は、その関数の中でしか使用出来ないローカル(局所)変数となります。


タイマーの2重起動の防止

メニューの開始を素早く何回もクリックすると、その回数だけタイマーが起動してしまいます。1つのタイマーが起動したら、このタイマーが動いている間は他のタイマーが起動できないようにするのが良いでしょう。この場合、タイマーが起動しているかどうかを記憶しておく変数(f_time)を1個作る必要があります。まず、この変数をメンバー変数として登録します。 timer.hを開いて、クラスの定義class CMainFrame : public CFrameWndのなかで、

private:
	MMRESULT uTimerID;
	BOOL f_time;
のようにf_time変数を追加します。なお、BOOLはFALSEとTRUEの2つの値だけをとります。次にこの変数を初期化します。初期化はコンストラクタCMainFrame::CMainFrame関数の中の適当なところにf_time=FALSE;なる1行を追加します。そして
void CMainFrame::OnStart() 
{
	if(f_time==FALSE){
		f_time=TRUE;
		uTimerID=timeSetEvent(1000,0,Ring,reinterpret_cast<DWORD>(this), TIME_PERIODIC);
	}
}
のように、タイマーを起動するときにf_timeの値をTUREとします。これで、f_timeの値が=TRUEの時はtimeSetEvent関数が呼ばれないので、タイマーの2重起動が防止できます。

最後に、ウィンドウを閉じる時にタイマーを止める訳ですから、礼儀として、次のようf_timeの値をFALSEに戻しておきます。

CMainFrame::~CMainFrame()
{
	f_time=FALSE;
	timeKillEvent(uTimerID);
}

参考までに出来上がったプログラムはtimer1new.zipです。


戻る