在 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
----------------------------------------
注意事项
在实现过程中,我遇到了一些挑战,以下是关键注意事项:
- COM 资源管理:
- 必须正确初始化和释放 COM(如 CoInitializeEx 和 CoUninitialize),否则会导致内存泄漏。
- 每个 COM 对象(如 IWbemLocator)需调用 Release。
- SAFEARRAY 解析:
- WmiMonitorID 返回的 VARIANT 类型是 VT_ARRAY | VT_I4,表示 4 字节整数数组。
- 数据中每个有效字节后有零填充(如 {76, 0, 0, 0, 69}),需提取低字节并跳过零值。
- 错误处理:
- 检查每个 HRESULT 返回值(如 FAILED(hres)),确保程序健壮。
- 示例中未完全打印错误日志,实际应用中应添加详细日志。
- 权限要求:
- 访问 WMI 需管理员权限,运行时需提升权限(例如通过 VS 设置 UAC 或以管理员身份运行命令行)。
- 数据一致性:
- 与 PowerShell 或 Python 输出对比,确保 C++ 结果正确。
与 Python 的对比
相比 Python(通过 wmi 模块),C++ 的实现:
- 优点:性能更高,控制更细,适合嵌入式或高频应用。
- 缺点:代码复杂,需手动管理资源和解析 SAFEARRAY。
- Python 示例:我在之前的博客中介绍了如何用 Python 获取显示器信息,详见 如何在多显示器环境中精准标识和控制特定显示器_defultmonitor-CSDN博客。Python 代码更简洁,但依赖外部模块且性能稍逊:
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++ 系统编程有所帮助!有问题或改进建议,欢迎留言讨论。