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^;)