今年の愛用ツールの紹介。「3 年で飽きるアドベントカレンダー」、15 日目の記事です。
仕事量を見える化したい
仕事量を見える化したい という思いから打鍵の量を測ることにした。何事も計測である。
タイプ数カウンターを1日動かしてみた - Jorgeの日記
業務1日のキータッチ数をカウントしてみた|ようへい
自分の1234日分 打鍵数2700万回のキー別集計結果 #Keyboard - Qiita
ある日の私のタイプ数カウンターは以下のようになっている。
最大 30000 超というのはかなりの打鍵なのでこれは働いたと言って良いだろう。
タイプ数カウンター: タイプ数カウンターの詳細情報 : Vector ソフトを探す! は気に入っているのだが、この結果をほかに流用したり継続して使うにはデータの持ち運びにもう一工夫が必要だと思っている。思っているがちょうどいいものを探すにも手間がかかる。
なら作ろう、と言うことで代替の PowerShell です。
PowerShell を作ってみた
およそ ChatGpt の提案のものである。
# CountKeystrokes.ps1
# 1日のキー押下数をカウントし、CSVに保存する(Windows標準のみ)
# 実行方法: powershell -ExecutionPolicy Bypass -File .\CountKeystrokes.ps1
param(
[string]$LogDir = "$env:USERPROFILE\KeystrokeLogs",
[switch]$LogDetail = $true # 付けるとアプリ名+キー+タイムスタンプをCSVに詳細記録
)
# ログ準備
$today = Get-Date -Format "yyyy-MM-dd"
$logPath = Join-Path $LogDir "keystrokes_$today.csv"
$summaryPath = Join-Path $LogDir "summary_$today.txt"
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null }
if (-not (Test-Path $logPath)) {
"timestamp,app,virtKey,scanCode,keyName" | Out-File -FilePath $logPath -Encoding utf8
}
# アクティブウィンドウのタイトル(アプリ名)取得
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class Win32Window
{
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
public static string GetActiveWindowTitle() {
const int nChars = 1024;
StringBuilder Buff = new StringBuilder(nChars);
IntPtr handle = GetForegroundWindow();
if (GetWindowText(handle, Buff, nChars) > 0) return Buff.ToString();
return "";
}
}
"@
# グローバルキーボードフック(WH_KEYBOARD_LL)
Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public class KeyboardHook : IDisposable
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private LowLevelKeyboardProc _proc;
private IntPtr _hookID = IntPtr.Zero;
public event EventHandler<KeyPressedEventArgs> KeyPressed;
public KeyboardHook() {
_proc = HookCallback;
_hookID = SetHook(_proc);
}
public void Dispose() {
UnhookWindowsHookEx(_hookID);
}
private IntPtr SetHook(LowLevelKeyboardProc proc) {
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule) {
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
}
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN)) {
KBDLLHOOKSTRUCT kb = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
KeyPressed?.Invoke(this, new KeyPressedEventArgs(kb.vkCode, kb.scanCode));
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT {
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
}
public class KeyPressedEventArgs : EventArgs
{
public uint VkCode { get; private set; }
public uint ScanCode { get; private set; }
public KeyPressedEventArgs(uint vk, uint sc) { VkCode = vk; ScanCode = sc; }
}
"@
# 仮想キーコード → 名前
$vkMap = @{
8='Backspace'; 9='Tab'; 13='Enter'; 16='Shift'; 17='Ctrl'; 18='Alt';
19='Pause'; 20='CapsLock'; 27='Esc'; 32='Space'; 33='PageUp'; 34='PageDown'; 35='End'; 36='Home';
37='Left'; 38='Up'; 39='Right'; 40='Down'; 44='PrintScreen'; 45='Insert'; 46='Delete';
48='0';49='1';50='2';51='3';52='4';53='5';54='6';55='7';56='8';57='9';
65='A';66='B';67='C';68='D';69='E';70='F';71='G';72='H';73='I';74='J';75='K';76='L';77='M';
78='N';79='O';80='P';81='Q';82='R';83='S';84='T';85='U';86='V';87='W';88='X';89='Y';90='Z';
91='Win';92='Win';93='Apps';96='Num0';97='Num1';98='Num2';99='Num3';100='Num4';101='Num5';
102='Num6';103='Num7';104='Num8';105='Num9';106='Num*';107='Num+';109='Num-';110='Num.';111='Num/';
112='F1';113='F2';114='F3';115='F4';116='F5';117='F6';118='F7';119='F8';120='F9';121='F10';122='F11';123='F12';
}
# カウンタ
[int]$count = 0
$lastPrint = Get-Date
$hook = New-Object KeyboardHook
# イベントハンドラ
$handler = [System.EventHandler[KeyPressedEventArgs]]{
param($sender, $e)
$script:count++
if ($LogDetail) {
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$app = [Win32Window]::GetActiveWindowTitle()
$vk = [int]$e.VkCode
$scan = [int]$e.ScanCode
$name = $vkMap[[int]$vk]; if (-not $name) { $name = "VK_$vk" }
"$ts,$app,$vk,$scan,$name" | Add-Content -Path $logPath -Encoding utf8
}
# 2秒ごとに簡易表示更新
if ((Get-Date) - $lastPrint -gt (New-TimeSpan -Seconds 2)) {
Write-Host ("{0} 今日のタイプ数: {1}" -f (Get-Date -Format "HH:mm:ss"), $script:count) -ForegroundColor Cyan
$lastPrint = Get-Date
}
}
$hook.add_KeyPressed($handler)
Write-Host "キー入力監視を開始しました。Ctrl+C で終了します。" -ForegroundColor Green
Write-Host "ログ保存先: $logPath"
# メッセージループ(CPU負荷を抑える)
try {
while ($true) {
Start-Sleep -Milliseconds 50
}
}
finally {
# 終了時にサマリ保存
"date=$today; total_keystrokes=$count" | Out-File -FilePath $summaryPath -Encoding utf8
$hook.remove_KeyPressed($handler)
$hook.Dispose()
Write-Host "終了しました。今日のタイプ数: $count" -ForegroundColor Yellow
Write-Host "Summary: $summaryPath"
}
まとめ
以上、働いている日の私は 30000 回もキーを打っているとかすごいなあという印象。
PowerShell はこんな感じになります。
以上です~


