👋 はじめに
この記事では、前回の記事(【2】ロック・スリープ復帰を検知する)の実装に「AC 電源が抜かれた(バッテリー駆動に切り替わった)」イベントの検知を追加します。
ノートPCをオフィス外に持ち出す際、AC電源が外れたタイミングを検知できれば、「持ち出し開始」のトリガーとして活用できます。例えば、VPN切断の自動化やセキュリティポリシーの切り替え、資産管理ログへの記録などが考えられます。
🔌 AC 電源検知の仕組み
WM_POWERBROADCAST メッセージの wParam が PBT_APMPOWERSTATUSCHANGE (0x000A) になったとき、GetSystemPowerStatus API を呼んで現在の AC 接続状態を確認します。
WM_POWERBROADCAST
└─ wParam == PBT_APMPOWERSTATUSCHANGE (0x000A)
↓
GetSystemPowerStatus()
↓
ACLineStatus == 0 → AC 電源 OFF(バッテリー駆動)
ACLineStatus == 1 → AC 電源 ON(AC 駆動)
ACLineStatus の値 |
意味 |
|---|---|
0 |
オフライン(AC 電源 OFF = バッテリー駆動) |
1 |
オンライン(AC 電源 ON) |
255 |
不明 |
🔧 実装
以下のコードは、前回(【2】)で作成した NativeMessageWindow と Worker.cs に対する追加・変更です。差分箇所を示した後、変更後の全体コードも掲載しています。
🆕 追加する Win32 API
private const int PBT_APMPOWERSTATUSCHANGE = 0x000A; // ← 新規追加
[StructLayout(LayoutKind.Sequential)]
private struct SYSTEM_POWER_STATUS
{
public byte ACLineStatus; // 0: Offline, 1: Online, 255: Unknown
public byte BatteryFlag;
public byte BatteryLifePercent;
public byte SystemStatusFlag;
public uint BatteryLifeTime;
public uint BatteryFullLifeTime;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetSystemPowerStatus(out SYSTEM_POWER_STATUS lpSystemPowerStatus);
📝 WndProc への追加
case WM_POWERBROADCAST:
var evt = m.WParam.ToInt32();
// スリープ復帰(従来の S3 環境)
if (evt == PBT_APMRESUMEAUTOMATIC || evt == PBT_APMRESUMESUSPEND)
{
_onSessionChange("PowerResume");
break;
}
// AC 電源状態の変化 ← 新規追加
if (evt == PBT_APMPOWERSTATUSCHANGE)
{
if (GetSystemPowerStatus(out var status))
{
if (status.ACLineStatus == 0)
_onAcPowerOff(); // AC 電源 OFF(バッテリー駆動)
else if (status.ACLineStatus == 1)
_onAcPowerOn(); // AC 電源 ON
}
}
break;
⚙️ Worker.cs(全体)
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Project3_PowerEvents.Services;
namespace Project3_PowerEvents;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly FileLogService _fileLogService;
public Worker(ILogger<Worker> logger, FileLogService fileLogService)
{
_logger = logger;
_fileLogService = fileLogService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
LogEvent("STARTUP", "Windows Service が起動しました。");
var messageLoopThread = new Thread(RunMessageLoop)
{
IsBackground = true,
Name = "Win32MessageLoop"
};
messageLoopThread.SetApartmentState(ApartmentState.STA);
messageLoopThread.Start();
try
{
await Task.Delay(Timeout.Infinite, stoppingToken);
}
catch (OperationCanceledException) { }
finally
{
Application.Exit();
LogEvent("SHUTDOWN", "Windows Service が停止しました。");
}
}
private void RunMessageLoop()
{
using var window = new NativeMessageWindow(
onSessionChange: reason =>
LogEvent("SESSION_CHANGE", $"セッションが切り替わりました。理由: {reason}"),
onAcPowerOff: () =>
LogEvent("AC_POWER_OFF", "AC 電源が切断されました(バッテリー駆動に切り替わりました)。"),
onAcPowerOn: () =>
LogEvent("AC_POWER_ON", "AC 電源が接続されました。"),
logger: _logger
);
Application.Run();
}
private void LogEvent(string eventType, string message)
{
_fileLogService.WriteLog(eventType, message);
_logger.LogInformation("[{EventType}] {Message}", eventType, message);
}
}
🪟 NativeMessageWindow(全体)
internal sealed class NativeMessageWindow : NativeWindow, IDisposable
{
private const int WM_WTSSESSION_CHANGE = 0x02B1;
private const int WM_POWERBROADCAST = 0x0218;
private const int PBT_APMPOWERSTATUSCHANGE = 0x000A; // AC/バッテリー切替
private const int PBT_APMRESUMEAUTOMATIC = 0x0012;
private const int PBT_APMRESUMESUSPEND = 0x0007;
private const int WTS_SESSION_LOGON = 0x5;
private const int WTS_SESSION_UNLOCK = 0x8;
private const int NOTIFY_FOR_ALL_SESSIONS = 1;
[DllImport("Wtsapi32.dll", SetLastError = true)]
private static extern bool WTSRegisterSessionNotification(IntPtr hWnd, int dwFlags);
[DllImport("Wtsapi32.dll", SetLastError = true)]
private static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
private struct SYSTEM_POWER_STATUS
{
public byte ACLineStatus;
public byte BatteryFlag;
public byte BatteryLifePercent;
public byte SystemStatusFlag;
public uint BatteryLifeTime;
public uint BatteryFullLifeTime;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetSystemPowerStatus(out SYSTEM_POWER_STATUS lpSystemPowerStatus);
private readonly Action<string> _onSessionChange;
private readonly Action _onAcPowerOff;
private readonly Action _onAcPowerOn;
private readonly ILogger _logger;
public NativeMessageWindow(
Action<string> onSessionChange,
Action onAcPowerOff,
Action onAcPowerOn,
ILogger logger)
{
_onSessionChange = onSessionChange;
_onAcPowerOff = onAcPowerOff;
_onAcPowerOn = onAcPowerOn;
_logger = logger;
CreateHandle(new CreateParams { Caption = "Win32MessageWindow" });
if (!WTSRegisterSessionNotification(Handle, NOTIFY_FOR_ALL_SESSIONS))
logger.LogWarning("WTSRegisterSessionNotification failed.");
}
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_WTSSESSION_CHANGE:
var reason = m.WParam.ToInt32();
if (reason == WTS_SESSION_LOGON || reason == WTS_SESSION_UNLOCK)
_onSessionChange(reason == WTS_SESSION_LOGON ? "SessionLogon" : "SessionUnlock");
break;
case WM_POWERBROADCAST:
var evt = m.WParam.ToInt32();
if (evt == PBT_APMRESUMEAUTOMATIC || evt == PBT_APMRESUMESUSPEND)
{
_onSessionChange("PowerResume");
break;
}
if (evt == PBT_APMPOWERSTATUSCHANGE)
{
if (GetSystemPowerStatus(out var status))
{
if (status.ACLineStatus == 0) _onAcPowerOff();
else if (status.ACLineStatus == 1) _onAcPowerOn();
}
}
break;
}
base.WndProc(ref m);
}
public void Dispose()
{
WTSUnRegisterSessionNotification(Handle);
DestroyHandle();
}
}
📊 検知できるイベント一覧
| イベント | 検出方法 | ログの eventType |
|---|---|---|
| 起動時 |
ExecuteAsync 開始時 |
STARTUP |
| ロック解除 | WM_WTSSESSION_CHANGE (SessionUnlock) |
SESSION_CHANGE |
| スリープ復帰(モダンスタンバイ) | WM_WTSSESSION_CHANGE (SessionUnlock) |
SESSION_CHANGE |
| スリープ復帰(S3) | WM_POWERBROADCAST (PowerResume) |
SESSION_CHANGE |
| AC 電源 OFF |
WM_POWERBROADCAST (PBT_APMPOWERSTATUSCHANGE) + ACLineStatus == 0
|
AC_POWER_OFF |
| AC 電源 ON |
WM_POWERBROADCAST (PBT_APMPOWERSTATUSCHANGE) + ACLineStatus == 1
|
AC_POWER_ON |
| 停止時 |
ExecuteAsync の finally |
SHUTDOWN |
✅ 動作確認
| 確認内容 | 手順 | 期待ログ |
|---|---|---|
| AC 電源 OFF | AC アダプターを抜く | [AC_POWER_OFF] AC 電源が切断されました... |
| AC 電源 ON | AC アダプターを挿す | [AC_POWER_ON] AC 電源が接続されました。 |
ログ出力例:
[2026-06-03 09:00:00] [STARTUP] Windows Service が起動しました。
[2026-06-03 10:15:00] [AC_POWER_OFF] AC 電源が切断されました(バッテリー駆動に切り替わりました)。
[2026-06-03 10:20:00] [AC_POWER_ON] AC 電源が接続されました。
AC アダプターを抜き挿しするだけで確認できます。デスクトップ PC(バッテリーなし)では検知しません。
⚠️ 注意点
PBT_APMPOWERSTATUSCHANGE は AC 接続状態が変化するたびに発火します。ただし OS の判定タイミングによっては、抜いた直後に一瞬 ACLineStatus == 0 → 1 → 0 と連続で届くことがあります。頻繁なログが問題になる場合は、直近の状態をキャッシュしてデバウンス処理を入れると良いでしょう。
📝 まとめ
WM_POWERBROADCAST の PBT_APMPOWERSTATUSCHANGE と GetSystemPowerStatus の組み合わせで、AC 電源の抜き挿しをリアルタイムに検知できます。前回のセッションイベント検知と合わせることで、「ロック解除」「スリープ復帰」「AC 電源 OFF」という3つの主要なPC状態変化をカバーできるようになりました。
デスクトップPCではバッテリーがないため検知できない点と、OS のタイミングによる連続発火にはデバウンス処理で対応する点だけ注意してください。
📚 参考
- WM_POWERBROADCAST - Microsoft Learn
- PBT_APMPOWERSTATUSCHANGE - Microsoft Learn
- GetSystemPowerStatus - Microsoft Learn
- SYSTEM_POWER_STATUS - Microsoft Learn
本記事に掲載している内容は、私個人の見解であり、所属する組織の立場や戦略、意見を代表するものではありません。