用 C++ 获取显示器信息:深入 WMI 与 COM 接口

在 Windows 系统中,获取显示器信息(如制造商、序列号和产品代码)是一项常见任务。本文将展示如何使用 C++ 通过 Windows Management Instrumentation (WMI) 和 Component Object Model (COM) 接口实现这一功能。我们将以 WmiMonitorID 类为例,逐步构建一个健壮的程序,并分享实现过程中的关键注意事项。

背景

显示器信息通常存储在硬件的 EDID (Extended Display Identification Data) 中,可以通过操作系统接口访问。WMI 提供了一个高层次的查询机制,而 C++ 通过 COM 接口直接与之交互,相较于 Python 等语言,C++ 提供了更高的性能和控制力,但也带来了额外的复杂性。

我们的目标是从 WmiMonitorID 类获取以下字段:

  • Manufacturer:制造商名称(如 "LEC")。
  • SerialNumber:序列号(如 "GK1CKYP3")。
  • ProductCode:产品代码(如 "P782")。

实现步骤

以下是实现的核心步骤:

1. 初始化 COM 环境

COM 是 Windows 的组件对象模型,WMI 依赖它运行。我们需要:

  • 调用 CoInitializeEx 初始化 COM。
  • 设置安全级别以访问 WMI。

2. 创建 WMI 连接

通过 IWbemLocator 和 IWbemServices 接口连接到 WMI 的 "root\WMI" 命名空间。

3. 执行查询

使用 WQL (WMI Query Language) 查询 WmiMonitorID 类,获取显示器数据。

4. 解析数据

WMI 返回的数据存储在 SAFEARRAY 中,类型为 VT_ARRAY | VT_I4,需提取低字节并转换为字符串。

5. 获取显示设备信息

通过 EnumDisplayDevices 获取附加信息(如设备名称和驱动描述)。

6. 清理资源

释放 COM 对象和内存,确保程序无泄漏。

完整代码

以下是优化后的 C++ 代码,获取并显示完整的显示器信息:

#include <iostream>
#include <vector>
#include <string>
#include <windows.h>
#include <comdef.h>
#include <Wbemidl.h>
#include <iomanip> // 用于十六进制格式化
#pragma comment(lib, "wbemuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")

struct MonitorInfo {
    std::string Manufacturer;
    std::string SerialNumber;
    std::string ProductCode;
    std::string DeviceName;
    std::string DeviceString;
    DWORD StateFlags;
};

// Helper function to extract string from SAFEARRAY (VT_I4)
std::string extractStringFromSafeArray(SAFEARRAY* sa, bool hexOutput = false) {
    std::string result;
    if (!sa) return result;

    long lBound, uBound;
    SafeArrayGetLBound(sa, 1, &lBound);
    SafeArrayGetUBound(sa, 1, &uBound);

    INT* data;
    if (SUCCEEDED(SafeArrayAccessData(sa, (void**)&data))) {
        if (hexOutput) {
            // 只取第一个非零字节并转为十六进制
            for (long i = lBound; i <= uBound; ++i) {
                BYTE byteValue = static_cast<BYTE>(data[i] & 0xFF);
                if (byteValue != 0) {
                    std::ostringstream oss;
                    oss << "0x" << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byteValue);
                    result = oss.str();
                    break; // 只取第一个字节
                }
            }
        } else {
            // 提取所有非零字节作为字符串
            for (long i = lBound; i <= uBound; ++i) {
                BYTE byteValue = static_cast<BYTE>(data[i] & 0xFF);
                if (byteValue != 0 && byteValue < 128) {
                    result += static_cast<char>(byteValue);
                }
            }
            // Trim trailing spaces
            while (!result.empty() && result.back() == ' ') {
                result.pop_back();
            }
        }
        SafeArrayUnaccessData(sa);
    }
    return result;
}

std::vector<MonitorInfo> getMonitorInfo() {
    std::vector<MonitorInfo> monitorList;

    HRESULT hres = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (FAILED(hres)) return monitorList;

    hres = CoInitializeSecurity(
        nullptr, -1, nullptr, nullptr,
        RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,
        nullptr, EOAC_NONE, nullptr
    );
    if (FAILED(hres)) {
        CoUninitialize();
        return monitorList;
    }

    IWbemLocator* pLoc = nullptr;
    hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc);
    if (FAILED(hres)) {
        CoUninitialize();
        return monitorList;
    }

    IWbemServices* pSvc = nullptr;
    BSTR wmiNamespace = SysAllocString(L"ROOT\\WMI");
    hres = pLoc->ConnectServer(wmiNamespace, nullptr, nullptr, nullptr, 0, nullptr, nullptr, &pSvc);
    SysFreeString(wmiNamespace);
    if (FAILED(hres)) {
        pLoc->Release();
        CoUninitialize();
        return monitorList;
    }

    hres = CoSetProxyBlanket(
        pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
        RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
        nullptr, EOAC_NONE
    );
    if (FAILED(hres)) {
        pSvc->Release();
        pLoc->Release();
        CoUninitialize();
        return monitorList;
    }

    IEnumWbemClassObject* pEnumerator = nullptr;
    BSTR queryLanguage = SysAllocString(L"WQL");
    BSTR queryString = SysAllocString(L"SELECT * FROM WmiMonitorID");
    hres = pSvc->ExecQuery(queryLanguage, queryString, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, nullptr, &pEnumerator);
    SysFreeString(queryLanguage);
    SysFreeString(queryString);
    if (FAILED(hres)) {
        pSvc->Release();
        pLoc->Release();
        CoUninitialize();
        return monitorList;
    }

    IWbemClassObject* pclsObj = nullptr;
    ULONG uReturn = 0;

    while (pEnumerator) {
        HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
        if (0 == uReturn) break;

        VARIANT vtProp;

        std::string manufacturer;
        hr = pclsObj->Get(L"ManufacturerName", 0, &vtProp, 0, 0);
        if (SUCCEEDED(hr)) manufacturer = extractStringFromSafeArray(vtProp.parray);
        VariantClear(&vtProp);

        std::string serial;
        hr = pclsObj->Get(L"SerialNumberID", 0, &vtProp, 0, 0);
        if (SUCCEEDED(hr)) serial = extractStringFromSafeArray(vtProp.parray);
        VariantClear(&vtProp);

        std::string productCode;
        hr = pclsObj->Get(L"ProductCodeID", 0, &vtProp, 0, 0);
        if (SUCCEEDED(hr)) productCode = extractStringFromSafeArray(vtProp.parray, true); // 十六进制
        VariantClear(&vtProp);

        monitorList.push_back({ manufacturer, serial, productCode, "", "", 0 });
        pclsObj->Release();
    }

    pEnumerator->Release();
    pSvc->Release();
    pLoc->Release();
    CoUninitialize();

    std::vector<MonitorInfo> displayDevices;
    DWORD i = 0;
    DISPLAY_DEVICE dd = { 0 };
    dd.cb = sizeof(dd);
    while (EnumDisplayDevices(nullptr, i, &dd, 0)) {
        MonitorInfo info;
        info.DeviceName = dd.DeviceName;
        info.DeviceString = dd.DeviceString;
        info.StateFlags = dd.StateFlags;
        displayDevices.push_back(info);
        i++;
    }

    for (size_t i = 0; i < monitorList.size() && i < displayDevices.size(); ++i) {
        monitorList[i].DeviceName = displayDevices[i].DeviceName;
        monitorList[i].DeviceString = displayDevices[i].DeviceString;
        monitorList[i].StateFlags = displayDevices[i].StateFlags;
    }

    return monitorList;
}

int main() {
    std::vector<MonitorInfo> monitors = getMonitorInfo();
    if (!monitors.empty()) {
        for (size_t i = 0; i < monitors.size(); ++i) {
            std::cout << "Monitor " << (i + 1) << ":\n";
            std::cout << "  Manufacturer: " << monitors[i].Manufacturer << "\n";
            std::cout << "  SerialNumber: " << monitors[i].SerialNumber << "\n";
            std::cout << "  ProductCode: " << monitors[i].ProductCode << "\n";
            std::cout << "  DeviceName: " << monitors[i].DeviceName << "\n";
            std::cout << "  DeviceString: " << monitors[i].DeviceString << "\n";
            std::cout << "  StateFlags: " << monitors[i].StateFlags << "\n";
            std::cout << std::string(40, '-') << "\n";
        }
    } else {
        std::cout << "No monitors found.\n";
    }
    return 0;
}

编译方法:

g++ example.cpp -lole32 -loleaut32 -lwbemuuid -o example.exe

执行:

.\example.exe

显示显示器相关参数,测试代码是否正常:

Get-WmiObject -Namespace "root\WMI" -Class WmiMonitorID

显示指定参数

Get-WmiObject -Namespace "root\WMI" -Class WmiMonitorID | Format-List ManufacturerName, SerialNumberID, ProductCodeID

指定参数输出结果:

ManufacturerName : {76, 69, 67, 0...}
SerialNumberID   : {71, 75, 49, 67...}
ProductCodeID    : {50, 55, 56, 50...}

代码输出结果:

Monitor 1:
  Manufacturer: LEC
  SerialNumber: GK1CKYP3
  ProductCode: 0x32      -->ProductCodeID :50
  DeviceName: \\.\DISPLAY1
  DeviceString: Intel(R) UHD Graphics 630
  StateFlags: 5
----------------------------------------

注意事项

在实现过程中,我遇到了一些挑战,以下是关键注意事项:

  1. COM 资源管理
    • 必须正确初始化和释放 COM(如 CoInitializeEx 和 CoUninitialize),否则会导致内存泄漏。
    • 每个 COM 对象(如 IWbemLocator)需调用 Release。
  2. SAFEARRAY 解析
    • WmiMonitorID 返回的 VARIANT 类型是 VT_ARRAY | VT_I4,表示 4 字节整数数组。
    • 数据中每个有效字节后有零填充(如 {76, 0, 0, 0, 69}),需提取低字节并跳过零值。
  3. 错误处理
    • 检查每个 HRESULT 返回值(如 FAILED(hres)),确保程序健壮。
    • 示例中未完全打印错误日志,实际应用中应添加详细日志。
  4. 权限要求
    • 访问 WMI 需管理员权限,运行时需提升权限(例如通过 VS 设置 UAC 或以管理员身份运行命令行)。
  5. 数据一致性
    • 与 PowerShell 或 Python 输出对比,确保 C++ 结果正确。

与 Python 的对比

相比 Python(通过 wmi 模块),C++ 的实现:

import wmi
import win32api
# 使用 WMI 获取显示器的 EDID 数据
w = wmi.WMI(namespace="root\\WMI")
for monitor in w.WmiMonitorID():
manufacturer = "".join(chr(c) for c in monitor.ManufacturerName if c > 0) 
serial = "".join(chr(c) for c in monitor.SerialNumberID if c > 0)  
product_code = monitor.ProductCodeID     
monitor_list.append({
            "Manufacturer": manufacturer.strip(),
            "SerialNumber": serial.strip(),
            "ProductCode": hex(product_code[0]) if isinstance(product_code, tuple) and len(product_code) > 0 else None
        })

总结

通过 C++ 和 WMI 获取显示器信息是一次深入 Windows 系统编程的实践。掌握 COM 接口和 SAFEARRAY 解析是关键,合理的错误处理和资源管理能确保程序健壮性。如果你需要更灵活的输出格式(如十六进制 "0x32"),只需调整 extractStringFromSafeArray 的逻辑。

希望这篇博客对你理解 C++ 系统编程有所帮助!有问题或改进建议,欢迎留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值