初探Windows API - 切换分辨率的实现

直接调用Windows API是不安全的行为,所以相关操作都必须在unsafe块进行

获取当前计算机分辨率信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pub fn get_current_display_mode() -> Result<DisplayMode, DisplayError> {
    unsafe {
        let mut devmode: DEVMODEW = mem::zeroed();
        devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
        debug!(
            "DEVMODEW size: {} bytes, as u16: {}",
            mem::size_of::<DEVMODEW>(),
            mem::size_of::<DEVMODEW>() as u16
        );
       
        // 枚举指定显示设备的显示模式
        let result = EnumDisplaySettingsW(
            PCWSTR::null(), // 默认的显示设备
            ENUM_CURRENT_SETTINGS, // 当前正在使用的显示设置
            &mut devmode, // 函数会将获取到的当前显示设置填充到这个结构体中
        );
        debug!("EnumDisplaySettingsW result: {:?}", result);

// 检查 API 调用的成功与否
        if result.as_bool() {
            Ok(DisplayMode {
                width: devmode.dmPelsWidth,
                height: devmode.dmPelsHeight,
                refresh_rate: devmode.dmDisplayFrequency,
                bits_per_pixel: devmode.dmBitsPerPel,
            })
        } else {
            Err(DisplayError::EnumFailed)
        }
    }
}

Windows API 期望结构体以特定方式初始化,以免结构体中包含垃圾值

let mut devmode: DEVMODEW = mem::zeroed();

为什么?因为程序运行的时候,被分配的一块内存也有可能是上一个程序用过的,你只定义了结构体的一部分字段,但是其他字段可能还有内存本来存在的值。

所以要调用zeroed()方法用 0 字节填充所有字段,避免了使用未初始化内存的风险。

devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;

  • 为什么:
    • API 约定: Windows API 函数(如 EnumDisplaySettingsW)在接收 DEVMODEW 结构体时,需要知道这个结构体的大小,以便正确地读取和写入数据。dmSize 字段就是用来告知 API 结构体实际大小的。
    • 版本兼容性: DEVMODEW 结构体可能会在不同的 Windows 版本中有所变化(增加字段)。通过设置 dmSize,API 可以知道它正在处理的是哪个版本的结构体,从而避免读取超出实际大小的内存。
    • Rust 类型转换: mem::size_of::<DEVMODEW>() 返回的是 usize 类型,而 dmSize 需要的是 u16 类型,所以需要进行 as u16 的类型转换。

更改分辨率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
pub fn change_display_mode(mode: &DisplayMode, permanent: bool) -> Result<(), DisplayError> {
    unsafe {
        let mut devmode: DEVMODEW = mem::zeroed();
        devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
        devmode.dmPelsWidth = mode.width;
        devmode.dmPelsHeight = mode.height
        devmode.dmDisplayFrequency = mode.refresh_rate;
        devmode.dmBitsPerPel = mode.bits_per_pixel;
        devmode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL; // 指示有效字段
        debug!(
            "DEVMODEW size: {} bytes, as u16: {}",
            mem::size_of::<DEVMODEW>(),
            mem::size_of::<DEVMODEW>() as u16
        );

        let test_result = ChangeDisplaySettingsW(Some(&devmode), CDS_TEST);
        debug!("ChangeDisplaySettingsW (test) result: {:?}", test_result);

        if test_result != DISP_CHANGE_SUCCESSFUL {
            return Err(DisplayError::ChangeFailed(format!(
                "Test Failed: {:?}",
                test_result
            )));
        }

        // 除了CDS_UPDATEREGISTRY,其他标志都是临时的,系统重启后会恢复默认设置
        let flags = if permanent {
            CDS_UPDATEREGISTRY // 更新到注册表
        } else {
            CDS_TYPE(0) // 不是任何标志(空)
        };
        debug!("Changing display settings with flags: {:?}", flags);

        let result = ChangeDisplaySettingsW(Some(&devmode), flags);
        debug!("ChangeDisplaySettingsW result: {:?}", result);

        match result {
            DISP_CHANGE_SUCCESSFUL => Ok(()),
            DISP_CHANGE_RESTART => Err(DisplayError::ChangeFailed(
                "System restart required".to_string(),
            )),
            DISP_CHANGE_BADMODE => Err(DisplayError::ChangeFailed(
                "The graphics mode is not supported".to_string(),
            )),
            _ => Err(DisplayError::ChangeFailed(format!(
                "Unknown error: {:?}",
                result
            ))),
        }
    }
}

指定显示器版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
pub fn change_display_mode_for_monitor(
    device_name: String,
    mode: &DisplayMode,
    permanent: bool,
) -> Result<(), DisplayError> {
    let device_name_wide: Vec<u16> = OsStr::new(&device_name)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    debug!("Device name wide: {:?}", device_name_wide);

    unsafe {
        let mut devmode: DEVMODEW = mem::zeroed();
        devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
        devmode.dmPelsWidth = mode.width;
        devmode.dmPelsHeight = mode.height;
        devmode.dmDisplayFrequency = mode.refresh_rate;
        devmode.dmBitsPerPel = mode.bits_per_pixel;
        devmode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL;
        debug!(
            "DEVMODEW size: {} bytes, as u16: {}",
            mem::size_of::<DEVMODEW>(),
            mem::size_of::<DEVMODEW>() as u16
        );

        let test_result = ChangeDisplaySettingsExW(
            PCWSTR::from_raw(device_name_wide.as_ptr()),
            Some(&devmode),
            None,
            CDS_TEST,
            None,

        );

        debug!("ChangeDisplaySettingsExW (test) result: {:?}", test_result);
        if test_result != DISP_CHANGE_SUCCESSFUL {
            return Err(DisplayError::ChangeFailed(format!(
                "Test Failed: {:?}",
                test_result
            )));
        }


        let flags = if permanent {
            CDS_UPDATEREGISTRY
        } else {
            CDS_TYPE(0)
        };
        debug!("Changing display settings with flags: {:?}", flags);

        let result = ChangeDisplaySettingsExW(
            PCWSTR::from_raw(device_name_wide.as_ptr()),
            Some(&devmode),
            None,
            flags,
            None,
        );
        debug!("ChangeDisplaySettingsExW result: {:?}", result);

        match result {
            DISP_CHANGE_SUCCESSFUL => Ok(()),
            DISP_CHANGE_RESTART => Err(DisplayError::ChangeFailed(
                "System restart required".to_string(),
            )),
            DISP_CHANGE_BADMODE => Err(DisplayError::ChangeFailed(
                "The graphics mode is not supported".to_string(),
            )),
            _ => Err(DisplayError::ChangeFailed(format!(
                "Unknown error: {:?}",
                result
            ))),
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
pub fn restore_default_settings() -> Result<(), DisplayError> {
    unsafe {
        let result = ChangeDisplaySettingsW(None, CDS_TYPE(0));
        debug!("ChangeDisplaySettingsW result: {:?}", result);

        if result == DISP_CHANGE_SUCCESSFUL {
            Ok(())
        } else {
            Err(DisplayError::ChangeFailed(format!("恢复失败: {:?}", result)))
        }
    }
}

列举显示器

用于给change_display_mode_for_monitor()提供精确的显示器名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
pub fn enumerate_monitors() -> HashMap<String, String> {
    let mut monitors = HashMap::new();

    unsafe {
        let mut device: DISPLAY_DEVICEW = mem::zeroed();
        device.cb = mem::size_of::<DISPLAY_DEVICEW>() as u32;
        debug!(
            "DISPLAY_DEVICEW size: {} bytes, as u16: {}",
            mem::size_of::<DISPLAY_DEVICEW>(),
            mem::size_of::<DISPLAY_DEVICEW>() as u16
        );

        let mut device_index = 0u32;
        loop {
            debug!("Enumerating device index: {}", device_index);
            let result = EnumDisplayDevicesW(PCWSTR::null(), device_index, &mut device, 0);
            debug!("EnumDisplayDevicesW result: {:?}", result);

            if !result.as_bool() {
                debug!("No more devices found, breaking loop.");
                break;
            }

            // 检查是否是活动的显示器
            debug!("Device StateFlags: {:?}", device.StateFlags);
            if (device.StateFlags & DISPLAY_DEVICE_ACTIVE) != DISPLAY_DEVICE_STATE_FLAGS(0) {
                let monitor_name = String::from_utf16_lossy(
                    &device.DeviceName[..device
                        .DeviceName
                        .iter()
                        .position(|&c| c == 0)
                        .unwrap_or(device.DeviceName.len())],
                );
                let gpu_name = String::from_utf16_lossy(
                    &device.DeviceString[..device
                        .DeviceString
                        .iter()
                        .position(|&c| c == 0)
                        .unwrap_or(device.DeviceString.len())],
                );
                monitors.insert(monitor_name, gpu_name);
                debug!(
                    "Found active monitor: {}, using gpu name: {}",
                    String::from_utf16_lossy(&device.DeviceName),
                    String::from_utf16_lossy(&device.DeviceString)
                );
            }
            device_index += 1;
        }
        monitors
    }
}
  • EnumDisplayDevicesW: 这是核心的 Windows API 函数。
    • 第一个参数 PCWSTR::null(): 通常用于指定要枚举的设备路径。在这里传递 null 表示枚举所有与系统相关的显示设备。
    • 第二个参数 device_index: 当前要枚举的设备的索引。
    • 第三个参数 &mut device: 一个指向 DISPLAY_DEVICEW 结构体的可变引用,API 将把找到的设备信息填充到这个结构体中。
    • 第四个参数 0: 标志位,通常用于控制枚举行为。0 表示默认行为。