系统息屏亮屏相关操作
Jacksing

背景

我们当前的设备有个比较奇葩的地方,就是不会无法响应 Windows 的息屏亮屏操作。所以软件上的解决方案是,需要去监听 Windows 的相关息屏亮屏消息,然后通过硬件暴露出来的接口告知硬件,硬件再进行相应的变更。

设置息屏

设置息屏相对来说比较好处理,我们只需要广播一条息屏的消息出来,系统就会去响应。Windows 将这条消息放置在 WM_SYSCOMMAND 下,指定操作对象为 SC_MONITORPOWER,参数设置为 2 即可。具体代码如下:

1
SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);

在一般情况下,发生上面这条窗口消息都可以让屏幕息屏。但是,这是一个非常错误的做法!The Old New Thing 通过了一系列的博客阐述了一个概念,给桌面窗口发消息属于一种 hack 行为。这种行为可能会导致一些不可预知的问题,这条消息会被所有的 Top most 窗口处理,这意味着消息会被处理 N 次,虽然在大部分情况下这个广播消息不会有什么问题。但是类似的操作难免会在其他的地方给自己挖坑。需要记住一点的是,在 Windows 下,不要依赖不受控制的窗口去处理自己的事

正确的做法应该是创建一个自己的窗口,然后给这个窗口发送消息,由它来处理相关的事。

设置亮屏

在设置息屏的消息参数中,1 表示设置为亮屏。但是实际验证后发现,调用这个参数会让屏幕亮起来,然后不到两秒钟的时间就又息屏了。并不清楚这个问题的原因是啥,可能是因为系统?笔者使用的系统版本为 Windows 10 家庭中文版, 版本号为 22H2。但是 Google 了一波后发现,大家貌似都存在这样的问题。

针对这个问题,笔者考虑了一个比较取巧的方式来实现。既然移动鼠标能够让屏幕点亮,所以笔者的做法就是通过模拟鼠标移动来实现亮屏。并且还只能轻微移动,避免干扰用户的使用,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void TurnOnDisplay() {
INPUT inputs[2] = {};
ZeroMemory(inputs, sizeof(inputs));

inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE;
inputs[0].mi.dx = 0;
inputs[0].mi.dy = 1;

inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_MOVE;
inputs[1].mi.dx = 0;
inputs[1].mi.dy = -1;

SendInput(2, inputs, sizeof(INPUT));
}

mouse_event 这个接口会简单一些,但是 Windows 已经标记为废弃了。所以更换成了 SendInput 这个接口。

获取当前屏幕状态

设置相对来说还是一个比较简单的存在。但是获取当前屏幕状态就比较麻烦了。

Windows 貌似对显示器有点偏见,以前在实现显示器相关的需求的时候,就发现这一块操作的接口要么基本没有,要么就是只提供了一些很简单的能力。如果要获取非显示器设备的电源设置,文档上可以找到 GetDevicePowerState 这个接口。但是这个接口只能获取设备的电源状态,偏偏又不能获取显示器的电源状态。

所以在这一时找不到方案去获取息屏状态的情况下,笔者实现的方案是去枚举显示设备,然后根据枚举出来的设备一个个查询他们的电源数据。根据微软的定义,如果设备电源状态为 D0,表示设备是完全开启的。否则都是一些低功耗或休眠的状态。具体到显示器上,屏幕点亮的时候,设备必然处于 D0 状态。所以我们可以通过这个状态来判断显示器是否亮屏。

枚举设备,查询设备信息需要用到 SetupAPI 这一套东西。而设备主要是通过微软预定义的 GUID 来标识的,对于显示器,其 GUID 为 {4d36e96e-e325-11ce-bfc1-08002be10318}

有思路之后,处理起来其实就很简单了。通过 SetupDiGetClassDevsW 这一接口可以获取当前的设备集,而后利用 SetupDiEnumDeviceInfo 来枚举设备,对于每个设备,使用 SetupDiGetDeviceRegistryPropertyW 来读取设备电源数据。代码可以简单参考如下:

1
2
3
4
5
6
DEFINE_GUID(GUID_DEVCLASS_MONITOR, 0x4d36e96eL, 0xe325, 0x11ce, 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1,
0x03, 0x18);

bool GetMonitorState() {

}

通知

通知也是一个大麻烦,在 SetupAPI 中,笔者暂时没有找到合适的接口来注入通知回调。这个确实有点奇怪,这种数据的变更通知看起来是一个强需求,不过可能以窗口消息的形式对外暴露?后续可以研究一下。

在网上一通搜索之后,发现我们可以通过电源设置相关的接口来实现屏幕状态的变更通知。电源管理也是 Windows 整个体系内很大的课题,在我们的需求场景下,我们可以使用 RegisterPowerSettingNotification 这个接口来注册通知。该接口可以提供很多不同类型的电源状态变更通知,这些类型通过预设 GUID 来区分。对于屏幕的亮屏息屏状态,我们可以使用 GUID_CONSOLE_DISPLAY_STATE 这个 GUID 即可。而后,屏幕的状态变更会通过窗口消息 WM_POWERBROADCAST 来通知。

这个接口有两个有意思的地方。首先,屏幕除了息屏和灭屏两种状态外,还额外提供了一个灰显的状态,有这个状态的加持,整个接口就更加完备了。其次,在注册通知成功的时候,窗口消息会立马收到一个事件,告知当前的状态。在这一能力加持下,我们甚至可以不需要通过枚举显示设备的方式来获取当前的显示器状态。

参考链接

Determining the Monitor’s On/Off (sleep) Status

Power Setting GUIDs

how can i detect the Monitor state

Fumbling around in the dark and stumbling across the wrong solution

Device Power States

WM_POWERBROADCAST message

  • 本文标题:系统息屏亮屏相关操作
  • 本文作者:Jacksing
  • 创建时间:2024-04-15 12:39:09
  • 本文链接:https://wzzzx.github.io/Windows/system_screen_operation/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论