有朋友问我:怎么在自己的 Windows 程序里知道系统什么时候睡了、什么时候醒了?这问题听着冷门,实际场景却不少——比如日志审计要记录设备断电休眠时段,监控工具要在唤醒后第一时间补数据,有些低功耗盒子程序需要在睡眠前保存现场、唤醒后恢复任务。这里说的“睡眠唤醒事件”,指的就是 Windows 电源状态切换时系统广播给应用程序的那组通知,捕获它的常见路线有两条:一条是 C/C++ 走 Win32 消息循环,一条是 WPF 走 .NET 托管事件。两条路线我都实际写过,这篇就把原理、完整代码和几个容易踩的坑一次讲清楚,适合正在做桌面工具、系统守护程序或者物联网边缘设备控制面板的开发者参考。
1. 睡眠唤醒事件在系统里是怎么流转的
1.1 睡眠和休眠不是一回事,事件通知也不一样
先说底层的电源状态模型。传统 Windows 笔记本和台式机主要涉及 S3(挂起到内存)和 S4(休眠,挂起到磁盘)两种低功耗状态。我们日常说的“睡眠”绝大多数是 S3,内存保持供电,CPU 停止执行,唤醒后恢复速度很快;“休眠”是 S4,内存内容写入硬盘,彻底断电,恢复速度慢,但更省电。
系统进入这两种状态前、以及恢复后,会向所有顶层窗口广播一条 WM_POWERBROADCAST 消息。这条消息的 wParam 里带具体事件类型,最常见的几个:
| 事件常量 | 数值 | 含义 |
|---|---|---|
PBT_APMQUERYSUSPEND |
0x0000 | 系统询问是否允许进入睡眠,可返回 BROADCAST_QUERY_AND_DONT_SUSPEND 拒绝 |
PBT_APMQUERYSUSPENDFAILED |
0x0002 | 系统进入睡眠的请求被某个程序拒绝 |
PBT_APMSUSPEND |
0x0004 | 系统即将进入睡眠,此时已无法拒绝 |
PBT_APMRESUMECRITICAL |
0x0006 | 系统因临界状态恢复(比如电池耗尽),不会自动再发自动恢复通知 |
PBT_APMRESUMESUSPEND |
0x0007 | 用户通过按键等操作唤醒了系统 |
PBT_APMRESUMEAUTOMATIC |
0x0012 | 系统因定时器、网络事件等自动唤醒 |
一个关键点是:并不是所有程序都能收到这些广播。广播只发给顶层窗口,如果你的程序是控制台程序,或者没有创建任何窗口,那 WM_POWERBROADCAST 基本到不了你手里。这就是很多人写了个 while (true) { printf(...); } 然后发现死活收不到事件的原因。
1.2 事件触发的时序:睡前和醒后各自有几步
睡眠的完整流程大致是:用户点击睡眠或系统空闲超时后,系统先向每个顶层窗口发 PBT_APMQUERYSUSPEND,此时各程序可以拒绝。如果没有程序拒绝,系统接着发 PBT_APMSUSPEND,表示“我马上要睡了”,然后系统进入 S3/S4。
唤醒流程则是:系统从低功耗状态恢复后,先发 PBT_APMRESUMECRITICAL(仅在临界唤醒时有),然后发 PBT_APMRESUMEAUTOMATIC 或 PBT_APMRESUMESUSPEND。正常情况下收到 PBT_APMRESUMEAUTOMATIC 就代表系统已经完全可用。
这个时序对开发者的意义在于:如果程序在睡眠前有清理工作、保存状态、断开网络连接等需求,应该在 PBT_APMSUSPEND 里做;如果要恢复服务、重新建连、补拉数据,应该在唤醒事件里做,而不是靠轮询系统时间来判断。轮询不仅费电,而且在你被挂起的那段时间里,定时器根本不会触发,醒来的第一秒其实是“补偿式的一堆定时器同时到期”,乱得很。
另外注意一点:PBT_APMSUSPEND 发出后系统并不会无限等你,应用必须尽快处理完手头的事情返回,否则系统可能强制继续睡眠。所以睡眠事件里别做重活,哪怕你只是想在睡前把日志写完,也要保持轻量。
1.3 为什么很多程序收不到事件
收不到事件的原因主要有三种:
- 没有创建窗口,
WM_POWERBROADCAST无法投递; - 有窗口但窗口过程没有处理
WM_POWERBROADCAST并随手DefWindowProc了; - 程序跑在服务进程中,服务默认在 Session 0 里,和用户会话的电源状态广播隔离。
这些坑在下面两种语言实现里都会遇到,但解决方式不同,接下来分开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C/C++ 路线:用 Win32 消息循环接住广播
2.1 为什么用隐藏窗口而不是控制台程序
C/C++ 里最经典的接法就是创建一个顶层隐藏窗口,然后跑消息循环,在窗口过程里处理 WM_POWERBROADCAST。隐藏窗口不需要 UI,纯粹是为了让 Windows 有一个可以投递广播的地方。这就像你留了个收件地址,系统才可能把信送到你手上。
也可以不用窗口,直接调 RegisterPowerSettingNotification 注册回调,这属于更精细的一套 API,后面单独讲。先看消息循环方案,它可以让你彻底理解事件流转机制,而且代码依赖极小,一个 .c 文件就能编译跑起来。
2.2 最小可运行示例:隐藏窗口 + 消息循环
我自己常用的模板是这样,先注册窗口类,然后创建一个消息窗口(父窗口传 HWND_MESSAGE 也可以,但直接创建普通隐藏窗口更省事):
c复制#include <windows.h>
#include <stdio.h>
#include <tchar.h>
const wchar_t* kWindowClassName = L"PowerEventMonitorWindow";
// 事件名称转字符串,方便看日志
const char* PowerEventToString(DWORD evt)
{
switch (evt)
{
case PBT_APMQUERYSUSPEND: return "PBT_APMQUERYSUSPEND";
case PBT_APMQUERYSUSPENDFAILED: return "PBT_APMQUERYSUSPENDFAILED";
case PBT_APMSUSPEND: return "PBT_APMSUSPEND";
case PBT_APMRESUMECRITICAL: return "PBT_APMRESUMECRITICAL";
case PBT_APMRESUMESUSPEND: return "PBT_APMRESUMESUSPEND";
case PBT_APMRESUMEAUTOMATIC: return "PBT_APMRESUMEAUTOMATIC";
default: return "UNKNOWN";
}
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_POWERBROADCAST:
{
D
