[C#]ManicTime風な,使用アプリ記録ソフトを自作する



WindowsでManicTimeというソフトを使用すると、PCで使用していたアプリケーションのログを採取することが出来ます。PCでどんなアプリを何分使っていたかの作業ログを取る事で、無駄な時間をだらだら過ごしていないか自分の作業内容への振り返りをするきっかけになりまし、ライフログ的な使い方も出来ます。
ライフログの技術

ManicTimeはシンプルかつ分かりやすいUIで、気軽に使うには便利なのですが、採取したログを自分でExcelなどを使用して分析したかったので、同様の機能を持つ記録アプリを作成してみました。


今回作成するのは見た目の部分ではなく、裏方部分のデータ収集処理なので、GUIは最低限のものとなっています。見た目もこだわりたいのならChartコントロールを使用すれば派手なモノも作成できます。

なお、今回の開発環境にはVisualStudio 2010 + C#を使用しています。


C#による起動中プログラムのロギング方法

ManicTimeの仕組みは、1秒周期で最前面に出ているウィンドウのプログラム名&ウィンドウタイトルをロギングしています。

ですが、.Net FrameworkのAPIではアクティブなウィンドウを取る事が出来ないので、Win32APIのGetForegroundWindow()をコールしてProcess情報を取得します。
また、ウィンドウのタイトルも同様にWin32APIのGetWindowText()を使用します。


これらのAPIの使い方ですが、下記のサイトが詳しいので参考にしてみてください。
C#でアクティブウィンドウの取得
C#からWin32 APIをコールする方法


という訳で、ほとんど丸々コピーですが、下記のメソッドで使用しているプロセスの情報が採取します。

using System.Runtime.InteropServices;
...
 
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
 
[DllImport("user32.dll")]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
 
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int length);
 
//*********************************************************************
/// <summary> 現在最前面に表示されているウィンドウのProcessオブジェクトを取得
///             see also: http://mzs184.blogspot.jp/2009/03/c.html
/// </summary>
/// <returns></returns>
//*********************************************************************
public static Process GetActiveProcess()
{
    int id;
    // アクティブなウィンドウハンドルの取得
    IntPtr hWnd = GetForegroundWindow();
 
    // ウィンドウハンドルからプロセスIDを取得
    GetWindowThreadProcessId( hWnd, out id );
 
    Process process = Process.GetProcessById(id);
    return process;
}
 
//*********************************************************************
/// <summary> アクティブなウィンドウのタイトルを取得
///         see also: http://d.hatena.ne.jp/int128/20080110/1199975050
/// </summary>
/// <returns></returns>
//*********************************************************************
public static string GetActiveWindowTitle()
{
    IntPtr hWnd = GetForegroundWindow();
    StringBuilder title = new StringBuilder(1048);
 
    // ウィンドウタイトルを取得
    GetWindowText(hWnd, title, 1024);
    return title.ToString();
}




情報取得のメソッドを作成したら次はアクティブウィンドウ切り替わりタイミングを取ります。
アクティブなウィンドウが変わったことのイベントハンドラは無いので、Timerコントロールを使用して一定周期でポーリングします。

static private string prevProcName    = "";
static private string prevFilePath    = "";
static private string prevProductName = "";
 
//*********************************************************************
// 一定周期でログを取る
//*********************************************************************
private void timer1_Tick( object sender, EventArgs e ) {
    // 現在、最前面ウィンドウのプロセスオブジェクトを取得する
    Process proc = Process.GetCurrentProcess();
 
    string procName;
    string productName;
    string filePath;
 
    try { 
        procName =  GetActiveWindowTitle();
    } catch {
        procName = "unknown";
    }
 
    try { 
        filePath = proc.MainModule.FileName + Environment.NewLine;
    } catch {
        filePath = "unknown" + Environment.NewLine;
    }
 
    // ファイルのプロダクト情報(アプリによって登録状況が異なるので使用しない)
    try { 
        productName = proc.MainModule.FileVersionInfo.ProductName + Environment.NewLine;;
    } catch {
        productName = "unknown" + Environment.NewLine;
    }
 
    // アクティブなプロセスが変わってなければ何もしない
    if ( procName.Equals( prevProcName ) && 
         filePath.Equals( prevFilePath ) && 
         productName.Equals( prevProductName ) ) {
        return;
    }
 
    // 一件前の情報を覚える
    prevProcName = procName;
    prevFilePath = filePath;
    prevProductName = productName;
 
    // ログメッセージを組み立てる
    string logMessage = DateTime.Now.ToString() + "\t" + 
                        productName.Replace( "\t", " "  ).Replace( Environment.NewLine, "" ) + "\t" + 
                        procName.Replace( "\t", " " ).Replace( Environment.NewLine, "" ) + "\t" + 
                        filePath;
 
    WriteLog( logMessage );
    textLog.Text += logMessage;
}



このチェックは1秒程度の短い周期でチェックする事になるのですが、そんなに頻繁に使用プログラムの切り替えは行われないので、大抵の場合は同じプログラムです。ですので、ログを書いたときに記録したアプリケーションの情報を覚えておき、その情報が変わった時のみログファイルに落としておく事にします。



最後は、ログファイルの出力です。

//*********************************************************************
/// <summary> ログをファイルに出力する
/// </summary>
/// <param name="logMessage">出力するメッセージ</param>
//*********************************************************************
public static void WriteLog( string logMessage ) {
    try {
        string fileName = "log.txt";
 
 
        // ファイルにログを出力
        using ( FileStream   stream = new FileStream( fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite ) )
        using ( StreamWriter writer = new StreamWriter( stream, Encoding.GetEncoding(932) ) ) { 
            writer.WriteLine( logMessage );
        }
    } catch {
        // 所詮ログファイルなので、書き込みに失敗しても無視する
    }
}


日付単位でログをローテーションさせる。といった高度な処理は無く、ひたすら追記していきます。
これだと、ディスクを圧迫し続けるのでどこかでログのローテーションが必要なのですが、この辺は運用依存なので利用者に委ねる事にします。
おそらくlogrotateコマンド的なものでローテーションさせるのが良いかと思います。

有り触れたファイル出力処理ですが、ファイル書き込み中にログのローテーションが行われることを想定し、ファイルの共有モードをFileShare.ReadWriteにしています。
ファイルを排他使用しないことで、他プロセスからのアクセスを許容させます。


ファイル出力が目的なので、GUIは凝ったモノにはしません。
動作確認用のテキストボックスと、周期実行用のタイマーが貼り付けてあるだけです。



ダウンロード

今回作成したプログラムの実行ファイルと、ソースコードです。
ソース(VS2010プロジェクト):NanoTaskLogger.zip
プログラム(exe):NanoTaskLogger


ライフログビジネス

関連記事

コメントを残す

メールアドレスが公開されることはありません。