[HSP]マルチディスプレイで解像度などの情報を取得する revision

# はじめに
エア HSP Advent Calender 2019、1日目の記事です。
ディスプレイのデバイス名、位置、解像度、リフレッシュレート、スケールを表示します。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151962/55fe9639-7902-9689-1c38-0d7ca2f0af6d.png)
[以前にブログに書いていた内容](https://uchitsukushi2.hatenablog.jp/entry/2016/06/18/193319) のバージョンアップ版です。HSPの掲示板でマルチディスプレイの質問を見かけて思い出し、久しぶりにスクリプトを実行したらDPIの異なる環境で描画内容がおかしなことになっていたので、その修正と情報取得の方法を少し変えて更新しました。その際に得た情報やスクリプトの説明などを書いています。

# スクリプト
HSP3.6β1 でテストを行っていますがおそらく 3.5 でも動くと思います。OSは Windows 10 バージョン1903 で確認しています。7 や 8 でも動くように書いたつもりですが確認はできていないのでご了承ください。

```hsp:dispinfo.hsp
#include "user32.as"
#include "gdi32.as"

#ifndef SetProcessDPIAware
#uselib "user32"
#func SetProcessDPIAware "SetProcessDPIAware"
#endif

#uselib "shcore"
#func SetProcessDpiAwareness "SetProcessDpiAwareness" int
#func GetScaleFactorForMonitor "GetScaleFactorForMonitor" int, int

#const PROCESS_PER_MONITOR_DPI_AWARE 2
#const DISPLAY_DEVICE_PRIMARY_DEVICE 4
#const DISPLAY_DEVICE_ACTIVE 1
#const ENUM_CURRENT_SETTINGS -1
#const MONITOR_DEFAULTTONULL 0
#const LOGPIXELSX 88
#const LOGPIXELSY 90
#const HALFTONE 4
#const SRCCOPY $00cc0020
#const TA_LEFT 0
#const TA_RIGHT 2
#const TA_CENTER 6

	if varptr(SetProcessDpiAwareness) {
		SetProcessDpiAwareness PROCESS_PER_MONITOR_DPI_AWARE
	} else : if varptr(SetProcessDPIAware) {
		SetProcessDPIAware
	}

	dim dd, 106 ; DISPLAY_DEVICE
	dd = 424 ; cb
	dupptr dd_name, varptr(dd) + 4, 32, 2 ; DeviceName
	dup dd_flag, dd(41) ; StateFlags

	dim dm, 40 ; DEVMODE
	dup dm_posx,   dm(11) ; dmPosition.x
	dup dm_posy,   dm(12) ; dmPosition.y
	dup dm_width,  dm(27) ; dmPelsWidth
	dup dm_height, dm(28) ; dmPelsHeight
	dup dm_freq,   dm(30) ; dmDisplayFrequency

	psLeft = 0
	psTop = 0
	psRight = 0
	psBottom = 0

	n = 0
	sf = 100 * GetDeviceCaps(hdc, LOGPIXELSX) / 96
	repeat
		if EnumDisplayDevices(0, cnt, varptr(dd), 0) == 0 : break
		if (dd_flag & DISPLAY_DEVICE_ACTIVE) == 0 : continue
		EnumDisplaySettings dd_name, ENUM_CURRENT_SETTINGS, varptr(dm)
		if varptr(GetScaleFactorForMonitor) {
			hm = MonitorFromPoint(dm_posx, dm_posy, MONITOR_DEFAULTTONULL)
			if hm == 0 : continue
			GetScaleFactorForMonitor hm, varptr(sf)
		}
		dname(n) = dd_name
		flag(n) = dd_flag & DISPLAY_DEVICE_PRIMARY_DEVICE
		px(n) = dm_posx
		py(n) = dm_posy
		sx(n) = dm_width
		sy(n) = dm_height
		hz(n) = dm_freq
		sc(n) = sf
		psLeft = min(psLeft, dm_posx)
		psTop = min(psTop, dm_posy)
		psRight = max(psRight, dm_posx + dm_width)
		psBottom = max(psBottom, dm_posy + dm_height)
		n++
	loop
	moniNum = n

	; screen 0, 1024, 768

	gsel
	redraw 0
	winsx = ginfo_sx
	winsy = ginfo_sy
	psSizeX = psRight - psLeft
	psSizeY = psBottom - psTop
	if (psSizeX > psSizeY * winsx / winsy) {
		rate = double(winsx - 40) / psSizeX
		dox = 20
		doy = winsy / 2 - rate * psSizeY / 2
	} else {
		rate = double(winsy - 40) / psSizeY
		dox = winsx / 2 - rate * psSizeX / 2
		doy = 20
	}

	font "arial", 12
	color 220, 220, 220
	gmode 5, , , 30
	box psLeft, psTop, psSizeX, psSizeY
	repeat moniNum
		if (flag(cnt)) {
			color 220, 170, 170
		} else {
			color 150, 200, 200
		}
		box px(cnt), py(cnt), sx(cnt), sy(cnt)
		w = x2 - x1
		h = y2 - y1

		buffer 1, w, h
		dc = CreateDC(dname(cnt), 0, 0, 0)
		SetStretchBltMode hdc, HALFTONE
		StretchBlt hdc, 0, 0, w, h, dc, 0, 0, sx(cnt), sy(cnt), SRCCOPY
		DeleteDC dc
		gsel
		pos x1, y1
		gcopy 1, 0, 0, w, h

		color
		pos x1 + 6, y1 + 4
		mes2 strf("(%d, %d)", px(cnt), py(cnt)), TA_LEFT
		x = x1 + w / 2
		y = y1 + h / 2
		pos x, y - 12
		mes2 dname(cnt), TA_CENTER
		pos x, y + 4
		mes2 strf("%dx%d, %dhz, %d%%", sx(cnt), sy(cnt), hz(cnt), sc(cnt)), TA_CENTER
		pos x2 - 6, y2 - 20
		mes2 strf("(%d, %d)", px(cnt) + sx(cnt) - 1, py(cnt) + sy(cnt) - 1), TA_RIGHT
	loop
	redraw 1

	stop

#deffunc box int _x, int _y, int _w, int _h
	x1 = dox + rate * (_x - psLeft)
	y1 = doy + rate * (_y - psTop)
	x2 = dox + rate * (_x + _w - psLeft)
	y2 = doy + rate * (_y + _h - psTop)
	boxf x1, y1, x2 - 1, y2 - 1
	color ginfo_r + 20, ginfo_g + 20, ginfo_b + 20
	boxf x1 + 4, y1 + 4, x2 - 5, y2 - 5
	return

#defcfunc min int _a, int _b
	if _a < _b : return _a : else : return _b

#defcfunc max int _a, int _b
	if _a > _b : return _a : else : return _b

#deffunc mes2 str _s, int _align
	s = _s
	SetTextAlign hdc, _align
	TextOut hdc, ginfo_cx, ginfo_cy, s, strlen(s)
	return
```

# インクルード・命令/定数の定義
`user32.as`と`gdi32.as`をインクルードしていますが、それとは別に`SetProcessDPIAware`などの命令を新たに定義しています。これらは比較的新しい関数で現在HSPに標準付属の定義ファイルには含まれていないため追加で定義します。一応今後定義ファイルが更新されてこれらの関数が含まれた場合を見越して`#ifndef`で多重定義対策も。
定数定義は標準付属されていないのでネットで調べて記述。個人的に定数を何度も使わない短いスクリプトの場合は直接数値書いてコメントで補足するのが好きなんですが、今回は種類もそこそこあるので初めにまとめて定義しました。MSDocsは定数の数値も書いててほしいな…

# 高DPI対応

```
	if varptr(SetProcessDpiAwareness) {
		SetProcessDpiAwareness PROCESS_PER_MONITOR_DPI_AWARE
	} else : if varptr(SetProcessDPIAware) {
		SetProcessDPIAware
	}
```

現在のHSPはデフォルトではDPI unawareで、高DPI時にはOSが自動的にスケーリングするようになっており、またginfo_mxなどで得られる座標やサイズ等もDPIに応じて変換された値が返ってきます。これによりDPIが違う環境でも大体同じ見た目や動作になるようになっています。上記のスクリプトの処理をするとDPI awareとなり変換されない値が返るようになります。OSによる自動スケーリングもなくなるため同じような見た目にするためには自前でスケーリングする必要があるのです。
今回のプログラムでは変換前の値で扱う部分があるためDPI awareに設定しています。`SetProcessDpiAwareness`と`SetProcessDPIAware`はどちらもDPI awareに設定する関数ですが、前者はモニター毎のDPIに対応、後者はシステムDPIに対応の違いです。関数呼び出し前に`varptr`でチェックしているのはその関数が使えるか調べています。これらの関数は新しめの機能で古い環境だと関数自体が存在しないためエラーになるからです。簡易的なOSバージョンチェックになります。
ちなみにDPI awareを設定するのはこのようにAPIを使うのではなくアプリケーションマニフェストで指定することが推奨されています。HSPでは`#packopt manifest`で実行ファイル作成時に埋め込むマニフェストを指定できます。

# 構造体定義

```
	dim dd, 106 ; DISPLAY_DEVICE
	dd = 424 ; cb
	dupptr dd_name, varptr(dd) + 4, 32, 2 ; DeviceName
	dup dd_flag, dd(41) ; StateFlags
```

HSPには構造体がないので、外部の関数を使う際に構造体が必要なときは配列などに割り当ててやりくりします。構造体の各メンバを取得するにはそのメンバがある位置を指定して読み取っていく必要があります。
配列から直接構造体の値は読み取れるのですが、[以前に見た記事内容](https://qiita.com/ksoft2015/items/e8329f011ec127e73b25) を活用してみたかったので、ここでは`dup`や`dupptr`を使い前もって配列の指定の要素のクローン変数を作り一箇所にまとめて定義しています。

# 仮想画面座標

```
	psLeft = 0
	psTop = 0
	psRight = 0
	psBottom = 0
```

これらの値は全てのディスプレイを覆う矩形座標です。前回は`GetSystemMetrics`で取得していたのですが、DPIが異なる環境だと値がズレていたので今回はディスプレイの位置や解像度情報から計算で求めることにしました。

```
		psLeft = min(psLeft, dm_posx)
		psTop = min(psTop, dm_posy)
		psRight = max(psRight, dm_posx + dm_width)
		psBottom = max(psBottom, dm_posy + dm_height)
```

repeat内にある計算部分です。各ディスプレイの位置・サイズから矩形座標を求めていきます。`min`と`max`関数はスクリプトの最後あたりで定義しています。

# 各ディスプレイ情報取得

1. `EnumDisplayDevices`でディスプレイのデバイス名を取得
2. `EnumDisplaySettings`にデバイス名を指定しディスプレイの位置・解像度・リフレッシュレートを取得
3. `MonitorFromPoint`にディスプレイの位置を指定しモニタハンドルを取得
4. `GetScaleFactorForMonitor`にモニタハンドルを指定してディスプレイの表示スケール（DPI倍率）を取得

の手順で情報を取得していきます。前回は`EnumDisplayMonitors`を使ったり`MonitorFromPoint`で座標を一つ一つ虱潰しに調べてモニタハンドルを得て`GetMonitorInfo`経由でデバイス名を得て`EnumDisplaySettings`に辿り着いていましたが、DEVMODE構造体にディスプレイの位置情報があることに気づき今回の手順になりました。（前回はなかったDPI倍率除けば）モニタのハンドルは必要なかったのです…
あと今回`EnumDisplayMonitors`で得た座標を`MonitorFromPoint`に指定してモニタハンドルを取っていますが、これはDPI awareでないと上手くいきません。`EnumDisplayMonitors`はDPIの変換を受けない素の座標が返りますが、`MonitorFromPoint`はDPIの影響を受けるため、高DPI時では座標がズレて取得に失敗するからです。今回はこのような方法を取っていますがモニタのハンドルを列挙するなら素直に`EnumDisplayMonitors`を使ったほうが良さそうです。

# デスクトップキャプチャ
後の部分は描画ですがほとんど前回と同じで色々計算しているのはウィンドウに収まるようにするためなので説明は省略します。
ただ一箇所デスクトップキャプチャ部分は変更があるのでここは説明を。

```
		buffer 1, w, h
		dc = CreateDC(dname(cnt), 0, 0, 0)
		SetStretchBltMode hdc, HALFTONE
		StretchBlt hdc, 0, 0, w, h, dc, 0, 0, sx(cnt), sy(cnt), SRCCOPY
		DeleteDC dc
		gsel
		pos x1, y1
		gcopy 1, 0, 0, w, h
```

前回は`GetDC 0`で全てのディスプレイを含むデバイスコンテキストからキャプチャしていましたが、久しぶりに実行してみたらデスクトップが重なっていたりしておかしなことになっていたため、ディスプレイ毎のデバイスコンテキストからキャプチャしてみると上手くいきました。また今回はキャプチャコピーで縮小する際ハーフトーンを使って小綺麗にしています。HSPの`gzoom`と同じやつです。

#おわりに
現状HSPは1つのメインディスプレイしか対応していないためサブディスプレイの情報がほしいときはWinAPIを使うしかない状況です。マルチ環境も増えてきたしその辺の対応もしてみたいと思ったのがきっかけで始めたプログラムですが、モニタハンドルの取得方法や高DPI対応、デスクトップキャプチャなど知らなかったことが収穫できた良い機会だったと思います。