PCの起動時間を記録してくれるソフトを作る

前回作ったDirectInputライブラリを、早速友人に試してみてもらったところ、
動かないよ−」「リンカエラー出るよ−」と言われた。
その原因は、VisualStudio側にありました。


・VS2005とVC++2008の罠
  見事にやられた。
  2005で作ったライブラリをVC++2008で使おうとすると、
  リンクエラーとか、その他よく分からないエラーが出たりして使えず。
  いろいろ原因を探った結果、
    ライブラリファイル自体をVC++2008でコンパイルし直せ!
  という事が分かり・・・
  VC++2008でコンパイルし直すと、確かに使えるようになった。
  これ、なんとかならないのかな・・・(汗)


・更にVC++2008の罠
  新規にプロジェクトを作成して、
  プロジェクト→プロパティ→構成プロパティ
  ここを見ても、何故かC/C++の項目が見あたらない。
  だが、一度保存して開きなおすと、何故か出現する。
  一体どういう事なのかと(ry


そんなこんなで、一応コンパイルにも成功し、
バイブに成功しないままライブラリの話題は終了してしまった。
また今度会った時のために、サンプルコードを書いておきたいんだけど
VC++2008のExpress Editionか、VS2008 英語版しか持ってない。
(e-Academyで落とせるのが何故か2008の英語版だったんだ・・・何故英語版。)
ま、まあ・・・きっと動くだろうきっと!うん。。


・起動時間を記録するソフトを作る
  さて、本題。
  普段使っていた起動時間を記録するソフトが、
  AntiVirをバージョンアップしたところ、何故かそのソフトを
  PCがフリーズしかけるほど検知しまくってくれるので
  (別に1回だけ検知すればいいじゃまいか・・・(^o^;))
  大変鬱陶しい事になってしまった。
  もともと24.9日↑連続で稼働させてると、起動時間がマイナスになっちゃう問題もあったので
  この際、自作してしまおうという感じ。


・必要な機能を挙げる
  次の項目を記録する。
   ・PCを起動した時間
   ・PCをシャットダウンした時間


  次の機能をつける。
   ・どれだけの時間、起動していたかを表で表示
   ・一番長く起動していた時間順に表示
   ・途中でPCの電源がぷっつり切れても大丈夫なように、
    一定時間ごとにデータを保存する機能
   ・上記機能の間隔のユーザによる設定


  各種仕様は次のような感じで。
   ・タスクトレイに格納して使用する。
   ・データはバイナリ形式で保存する。
   ・多重起動は行えない。
   ・C/C++STLを用いて作る。


・作るものを挙げる
  ・ダイアログリソース
  ・レコード構造体、設定構造体の定義
  ・レコードの追加・更新関数
  ・レコードの読み出し関数
  ・設定の保存・読み込み関数
  ・起動時間算出関数
   (24.9日↑の連続起動にも対応する!)


・ダイアログを作る
  著者の環境はシェルをExplorerではなくてBlackBoxに変えているので
  ウィンドウの表示に違和感を感じるかもしれないが、
  とりあえずこんな感じに作ってみた。




  各コントロールのIDは、
    IDD_ほにゃらら
    IDC_BTN_ほにゃらら
    IDC_EDIT_ほにゃらら
    IDC_LIST_ほにゃらら
  みたいな感じである。


・レコード構造体、設定構造体の定義
  windows.hをインクルードして・・・

typedef struct _tDate {
	BYTE	byYear;
	BYTE	byMonth;
	BYTE	byDay;
	BYTE	byHour;
	BYTE	byMinute;
	BYTE	bySecond;

} Date;

typedef struct _tRecord {
	Date	dtStart;
	ULONG64	lSpan;

} Record;

typedef struct _tConfig {
	DWORD	dwSpan;

} Config;


  ただのlongだと32ビットなので、ULONG64(unsigned __int64)を使用。


・レコード追加・更新関数
  dtStart (起動時刻)1つにつき、1つの lSpan (起動時間)が存在するので、
  もしレコードの末尾に追加しようとしたレコードと同じ dtStart を持つデータが存在する場合、
  勝手に lSpan のみ更新されるように処理する。
  ここで、dtStart構造体を比較するのに memcmp を使えば楽じゃないのだろうかと思ったのだけど
  ここにこういう事が書かれてたので
  素直に要素同士を比較する。


  fstreamを使って保存する。
  注意したいのが、モード。
  ios::out だけの場合、ファイルがまっさらになってしまう。
  だからといって ios::app をつけると、必ずファイル末尾に追記されるようになる。
  (seekpしても意味ない。)
  そこで、ios::out と ios::in の2つを一緒につける。
  そうする事で、ランダムアクセス+追記・上書きが行えるようになる。


  あと、末尾からシークする場合の落とし穴。

		ifs.seekg ( sizeof ( Record ), ios::end );

  はい。駄目な例。よく目に焼き付けましょう(^o^;)
  終端位置からレコード分だけ位置を下げるので、マイナス値にしないといけない。つまり、

		ifs.seekg ( -1 * sizeof ( Record ), ios::end );

  こうしないといけない。


  更に、今回の場合だと sizeof ( Record ) が正しい値を返さない。
  Dateが7バイト、ULONG64が8バイトなので、合計15バイト(0x0F)になるわけだが、
  sizeof が返す値は16バイト(0x10)
  これは多分、メモリ上に配置した際に1バイト分が空いちゃってるんだろうと思われる・・。
  仕方ないので、レコード長を手動で定義する事にする。


  結局2時間ぐらいかかって、ようやく追加・更新関数ができた。時間かかりすぎ。いい勉強になった。


  教訓:ランダムアクセスで追記・上書きするときは ios::in | ios::out
  肝に銘じます(^o^;)


・レコードの読み出し関数
  上記の件があるので、さくっと作っちゃう。
  一応 !ifs.eof ( ) の間だけループ回すけど、それだけだと不十分。
  1つ分のレコードを読み込んで、ifs.fail ( ) でチェックを行う。

	ifs.read ( (char*) &recTemp.dtStart.wYear, sizeof ( WORD ) );
	ifs.read ( (char*) &recTemp.dtStart.byMonth, sizeof ( BYTE ) );
	ifs.read ( (char*) &recTemp.dtStart.byDay, sizeof ( BYTE ) );
	ifs.read ( (char*) &recTemp.dtStart.byHour, sizeof ( BYTE ) );
	ifs.read ( (char*) &recTemp.dtStart.byMinute, sizeof ( BYTE ) );
	ifs.read ( (char*) &recTemp.dtStart.bySecond, sizeof ( BYTE ) );
	ifs.read ( (char*) &recTemp.lSpan, sizeof ( ULONG64 ) );
	if ( ifs.fail ( ) ) break;
	vecRecord.push_back ( recTemp );


  読み込みはこんだけ。ちなみにモードは ios::in | ios::binary。楽チン。


・設定の保存・読み込み関数
  保存する設定項目は、
    ・自動保存の間隔(秒)
  だけ。
  設定の場合、設定ファイルを開くときはまっさらにしてくれていいので、
  モードは ios::out | ios::binary で開いて保存します。


・起動時間算出関数
  24.9日↑に対応しないといけないので、少し工夫が必要。
  timeGetTime ( ) 関数を使って起動時間を取得するが、
  最大値を超えると0に戻ってしまう。
  そこで、前回の取得値を保持しておき、
  それと比較して少ない値だった場合は、ぐるっと一週してしまったという事で周回回数を1増やす。
  返す値は、周回回数×(2^32+1)+timeGetTime ( )とする。

ULONG64 GetRunningTime ( )
{
	static DWORD dwTime = 0;
	static BYTE  byCount = 0;
	
	DWORD	dwNowTime;
	ULONG64	lAns;

	dwNowTime = timeGetTime ( );
	if ( dwNowTime < dwTime ) {
		byCount++;

	}
	dwTime = dwNowTime;

	lAns = ( byCount * ( (ULONG64) 0xFFFFFFFF + 1 ) ) + dwNowTime;

	return lAns;

}


これで作るモノは作り終わったので、
後はごそごそとコーディングしていくだけ。へへへ。


・多重起動時の処理
  多重起動しようとしたときは、起動を中断する。
  多重起動の検知は、いくつか方法があるが、今回はMutexを用いる。

	HANDLE hMutex = 0;

	hMutex = CreateMutex ( NULL, FALSE, "RunningTimer" );

	if ( GetLastError ( ) == ERROR_ALREADY_EXISTS ) {
		// 多重起動時の処理

	}


  そして、終了時に ReleaseMutex で開放しておく。

	ReleaseMutex ( hMutex );


・リストビューの初期化
  適当に値を追加したりしながら、表示を整える。


case WM_INITDIALOG:
	g_hRecentList	= GetDlgItem ( hWnd, IDC_LIST_RECENT );
	g_hTopList	= GetDlgItem ( hWnd, IDC_LIST_TOP );
	lvcHeader.pszText	= "起動日時";
	lvcHeader.iSubItem	= 0;
	lvcHeader.cx		= 115;
	lvcHeader.fmt		= LVCFMT_LEFT;
	lvcHeader.mask		= LVCF_TEXT | LVCF_FMT | LVCF_SUBITEM | LVCF_WIDTH;
	SendMessage ( g_hRecentList, LVM_INSERTCOLUMN, 0, (LPARAM) &lvcHeader );
	SendMessage ( g_hTopList, LVM_INSERTCOLUMN, 0, (LPARAM) &lvcHeader );
		
	lvcHeader.pszText	= "終了日時";
	SendMessage ( g_hRecentList, LVM_INSERTCOLUMN, 1, (LPARAM) &lvcHeader );
	SendMessage ( g_hTopList, LVM_INSERTCOLUMN, 1, (LPARAM) &lvcHeader );
			
	lvcHeader.pszText	= "起動時間";
	lvcHeader.cx		= 130;
	lvcHeader.fmt		= LVCFMT_RIGHT;
	SendMessage ( g_hRecentList, LVM_INSERTCOLUMN, 
					2, (LPARAM) &lvcHeader );
	SendMessage ( g_hTopList, LVM_INSERTCOLUMN, 
					2, (LPARAM) &lvcHeader );

	dwStyle = (DWORD) SendMessage ( g_hRecentList, 
				LVM_GETEXTENDEDLISTVIEWSTYLE, 0L, 0L );
	dwStyle |= LVS_EX_GRIDLINES;
	SendMessage ( g_hRecentList, LVM_SETEXTENDEDLISTVIEWSTYLE,
				0L, (LPARAM) dwStyle );
	SendMessage ( g_hTopList, LVM_SETEXTENDEDLISTVIEWSTYLE, 
				0L, (LPARAM) dwStyle );


  よしよし、いい感じ。
  早速Recordを引数に取って、リストにアイテムを追加してくれる関数を作る。
  で、ここで急遽レコードの形式を変更する・・・(^o^;)
  当初は開始時刻、経過時間だけを記録する予定だったが、
  開始時刻と経過時間から終了時刻を算出するのがめんどいので
  終了時刻も記録しちゃう事にする。
  変更するところは、LoadRecordとSaveRecord関数だけ。
  しかも間にちょこっと挟むだけなので楽。


  自動保存は、SetTimerを使って定期的にWM_TIMERを受け取り、保存する。

	g_Config = LoadConfig ( );
	if ( g_Config.dwSpan < 1 ) {
		g_Config.dwSpan = 600;

	}

	SetTimer ( hWnd, 0, g_Config.dwSpan * 1000, NULL ); 


  あまりに長くなってきたので、今回はこの辺で。
  後やらなきゃいけない事メモ:
    ・タスクトレイにアイコン表示
    ・タスクトレイのアイコンからの復帰
    ・アイコン作成
    ・ウィンドウ表示中は起動時間をリアルタイムに更新する
    ・設定ダイアログ周り
    ・起動時間TOP10とかその辺の処理諸々
  まだ結構ありますなぁ(^o^;)