【高DPI】デスクトップの物理座標系を得るには？【WinAPI】



# はじめに
　オレはマニフェストのDPI設定が「なし（false）」の時でもデスクトップ矩形は物理座標系で欲しいんや！という奇特な人向け情報です。  

# デスクトップ矩形の種類
　デスクトップ矩形には仮想デスクトップ矩形と普通のデスクトップ矩形があります。

* モニター毎のデスクトップ矩形
    * モニターごとのデスクトップ矩形。さらにモニター矩形とワーク矩形（タスクバー領域を除いた矩形）があります。
* 仮想デスクトップ矩形
    * 使用中のモニターすべてを内包する、仮想的に一枚のデスクトップに見立てた時の矩形です。

## モニター毎のデスクトップ矩形
### モニター矩形・ワーク矩形 (論理座標系)

```c
HMONITOR hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

MONITORINFOEX monInfo;
monInfo.cbSize = sizeof(MONITORINFOEX);
GetMonitorInfo(hMonitor, &monInfo);

// 論理座標系のデスクトップ矩形
monInfo.rcMonitor;
// 論理座標系のワーク（タスクバー矩形を除外した）矩形
monInfo.rcWork;
```

　DPI設定が「モニターごとの高いDPI認識」以外では論理座標系の値になります。そこでMONITORINFOEXで取れるszDeviceを使って以下の方法で物理座標系の値を取得します。   

### モニター矩形 (物理座標系)

```c
DEVMODE devMode;
devMode.dmSize = sizeof(DEVMODE);
// dmFieldsにDM_POSITION、dmDriverExtraにPOINTのサイズを入れるとdmPositionが取得出来る
devMode.dmDriverExtra = sizeof(POINTL);
devMode.dmFields = DM_POSITION;
EnumDisplaySettings(monInfo.szDevice, ENUM_CURRENT_SETTINGS, &devMode);

// 物理座標系のデスクトップ矩形
RECT physicalDesktopRect;
physicalDesktopRect.left = devMode.dmPosition.x;
physicalDesktopRect.top = devMode.dmPosition.y;
physicalDesktopRect.right = devMode.dmPosition.x + devMode.dmPelsWidth;
physicalDesktopRect.bottom = devMode.dmPosition.y + devMode.dmPelsHeight;
```
　EnumDisplaySettingsという割と昔からあるAPIっぽいのですが、これで物理座標を取得可能です。 
　dmPositionに左上座標が、dmPelsWidthとdmPelsHeightはそれぞれ幅と高さが入っているので合成して矩形を作成しています。 
　Work座標は取得できないので、欲しい場合は計算する必要があります。 

### ワーク矩形 (物理座標系)

```c
SIZE logicalDesktopSize;
logicalDesktopSize.cx = monInfo.rcMonitor.right - monInfo.rcMonitor.left;
logicalDesktopSize.cy = monInfo.rcMonitor.bottom - monInfo.rcMonitor.top;

typedef struct{
    double cx;
    double cy;
}DSIZE;

// 物理座標と論理座標からスケールを出す
DSIZE scale;
scale.cx = (double)devMode.dmPelsWidth / (double)logicalDesktopSize.cx;
scale.cy = (double)devMode.dmPelsHeight / (double)logicalDesktopSize.cy;

// 物理座標系のワーク矩形
RECT physicalWorkRect;
physicalWorkRect.left = monInfo.rcWork.left * scale.cx;
physicalWorkRect.top = monInfo.rcWork.top * scale.cy;
physicalWorkRect.right = monInfo.rcWork.right * scale.cx;
physicalWorkRect.bottom = monInfo.rcWork.bottom * scale.cy;
```

　モニター矩形の物理座標系と論理座標系から係数をだして、論理座標系のワーク矩形を物理座標系に変換しています。  

## 仮想デスクトップ矩形 (論理座標系)
```c
// 仮想デスクトップ空間矩形
RECT rectVitualDesktop;
rectVitualDesktop.left = GetSystemMetrics(SM_XVIRTUALSCREEN);
rectVitualDesktop.top = GetSystemMetrics(SM_YVIRTUALSCREEN);
rectVitualDesktop.right = rectVitualDesktop.left + GetSystemMetrics(SM_CXVIRTUALSCREEN);
rectVitualDesktop.bottom = rectVitualDesktop.top + GetSystemMetrics(SM_CXVIRTUALSCREEN);
```
　DPI設定が「モニターごとの高いDPI認識」以外では論理座標系の値になります。 
　仮想デスクトップ矩形の物理座標を簡単に取得する方法が見つからなかったため計算で取得します。

## 仮想デスクトップ矩形 (物理座標系)

```c
typedef struct {
    RECT physicalMonitors[25];
    int count;
}ENUM_MONITOR_RECTS, PENUM_MONITOR_RECTS*;

BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lpsrMonitor, LPARAM lParam)
{
    PENUM_MONITOR_RECTS lpRects = (PENUM_MONITOR_RECTS)lParam;

	MONITORINFOEX monInfo;
	monInfo.cbSize = sizeof(MONITORINFOEX);
	GetMonitorInfo(hMonitor, &monInfo);

	DEVMODE devMode;
	devMode.dmSize = sizeof(DEVMODE);
	devMode.dmDriverExtra = sizeof(POINTL);
	devMode.dmFields = DM_POSITION;
	EnumDisplaySettings(monInfo.szDevice, ENUM_CURRENT_SETTINGS, &devMode);

	// 物理座標系のモニター矩形
	lpRects->physicalMonitors[lpRects->count].left = devMode.dmPosition.x;
	lpRects->physicalMonitors[lpRects->count].top = devMode.dmPosition.y;
	lpRects->physicalMonitors[lpRects->count].right = devMode.dmPosition.x + devMode.dmPelsWidth;
	lpRects->physicalMonitors[lpRects->count].bottom = devMode.dmPosition.y + devMode.dmPelsHeight;

	++lpRects->count;
	return TRUE;
}

RECT GetVirtualDesktopPhysicalRect()
{
	ENUM_MONITOR_RECTS s_lpRects = {0};
	EnumDisplayMonitors(NULL, NULL, EnumMonitorProc, (LPARAM)s_lpRects);


	RECT physicalVirtualDesktop = {0};
	for (int nCnt = 0; nCnt < monitorRects.count; ++nCnt) {
		physicalVirtualDesktop.left = min(monitorRects.physicalMonitors[nCnt].left, physicalVirtualDesktop.left);
		physicalVirtualDesktop.top = min(monitorRects.physicalMonitors[nCnt].top, physicalVirtualDesktop.top);
		physicalVirtualDesktop.right = max(monitorRects.physicalMonitors[nCnt].right, physicalVirtualDesktop.right);
		physicalVirtualDesktop.bottom = max(monitorRects.physicalMonitors[nCnt].bottom, physicalVirtualDesktop.bottom);
	}
	return physicalVirtualDesktop;
}
```

　EnumDisplayMonitors()を使ってすべてのモニターを列挙する時に物理座標を保持して、min/maxで最大矩形を取り出します。これが仮想デスクトップの物理座標です。

# さいごに
　古いAPIが何気に拡張されてたり隠された事実を目の当たりにしてWinAPIの闇の深さを垣間見た。

　※文章を自動翻訳しやすいように少し修正しました。