はじめに
エア HSP Advent Calender 2019、1日目の記事です。
ディスプレイのデバイス名、位置、解像度、リフレッシュレート、スケールを表示します。
以前にブログに書いていた内容 のバージョンアップ版です。HSPの掲示板でマルチディスプレイの質問を見かけて思い出し、久しぶりにスクリプトを実行したらDPIの異なる環境で描画内容がおかしなことになっていたので、その修正と情報取得の方法を少し変えて更新しました。その際に得た情報やスクリプトの説明などを書いています。
スクリプト
HSP3.6β1 でテストを行っていますがおそらく 3.5 でも動くと思います。OSは Windows 10 バージョン1903 で確認しています。7 や 8 でも動くように書いたつもりですが確認はできていないのでご了承ください。
#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には構造体がないので、外部の関数を使う際に構造体が必要なときは配列などに割り当ててやりくりします。構造体の各メンバを取得するにはそのメンバがある位置を指定して読み取っていく必要があります。
配列から直接構造体の値は読み取れるのですが、以前に見た記事内容 を活用してみたかったので、ここでは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
関数はスクリプトの最後あたりで定義しています。
各ディスプレイ情報取得
-
EnumDisplayDevices
でディスプレイのデバイス名を取得 -
EnumDisplaySettings
にデバイス名を指定しディスプレイの位置・解像度・リフレッシュレートを取得 -
MonitorFromPoint
にディスプレイの位置を指定しモニタハンドルを取得 -
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対応、デスクトップキャプチャなど知らなかったことが収穫できた良い機会だったと思います。