HSPでDPI変更時やモニターごとに高DPIに対応させる方法

## HSPでDPI変更時やモニターごとに高DPIに対応させる方法について
HSPで高DPIに対応させるには、Windows APIのSetProcessDpiAwareを呼び出すか、マニフェストを書き換えるのが一般的のようですが、今回はDPIの設定の変更時やモニターごとのDPIに対応させる方法についてです。
```DPI.hsp
#packopt name "High_DPI_Test"
#include "gdi32.as"
#include "user32.as"
#func global SetProcessDpiAwarenessContext "SetProcessDpiAwarenessContext" int
#func global SetProcessDPIAware "SetProcessDPIAware"
#func global AdjustWindowRectExForDpi "AdjustWindowRectExForDpi" int,int,int,int,int
#cfunc global GetDpiForSystem "GetDpiForSystem"
//#cfunc global GetDpiForWindow "GetDpiForWindow" int
#func global _GetSystemMetricsForDpi "GetSystemMetricsForDpi" int,int

//未使用だが、DPIが異なる別のモニターにウィンドウを作成する場合に必要。
/*
#uselib "Shcore.dll"
#func GetDpiForMonitor "GetDpiForMonitor" int,int,var,var
#func SetProcessDpiAwareness "SetProcessDpiAwareness" var
*/

#define SWP_NOSIZE 0x0001
#define SWP_NOMOVE 0x0002

#define DPI_AWARENESS_CONTEXT_UNAWARE	-1
#define DPI_AWARENESS_CONTEXT_SYSTEM_AWARE	-2
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE	-3
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2	-4
#define DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED	-5

#const SM_CYCAPTION		0x00000004
#const SM_CXFIXEDFRAME	0x00000007
#const SM_CYFIXEDFRAME	0x00000008
#const SM_CXSIZEFRAME	0x00000020
#const SM_CYSIZEFRAME	0x00000021
#const SM_CYSMCAPTION	0x00000033

#define WM_DPICHANGED 0x02E0
#define LOGPIXELSX	0x00000058
#define LOGPIXELSY	0x0000005A

#packopt manifest "DPI.manifest"
#packopt hide 1
#ifdef _debug
gsel 0,-1
//デバッグ時（F5実行）にもDPIを反映させる
opt = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2:SetProcessDpiAwarenessContext opt
//デバッグ時（F5実行）にマニフェストファイルを保存
exist "DPI.manifest"
if strsize <= 0{
mani = {"
<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\" xmlns:asmv3=\"urn:schemas-microsoft-com:asm.v3\">
<assemblyIdentity
version=\"1.0.0.0\"
processorArchitecture=\"*\"
name=\"OnionSoftware.hsp3.exe\"
type=\"win32\"
/>

<application xmlns=\"urn:schemas-microsoft-com:asm.v3\">
<windowsSettings>
<dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>
<dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>

<description>XPStyle</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type=\"win32\"
name=\"Microsoft.Windows.Common-Controls\"
version=\"6.0.0.0\"
processorArchitecture=\"*\"
publicKeyToken=\"6595b64144ccf1df\"
language=\"*\"
/>
</dependentAssembly>
</dependency>
<compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">
  <application>
    <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />
    <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />
    <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />
    <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />
  </application>
</compatibility>
</assembly>
"}
notesel mani
notesave "DPI.manifest"
}
#endif

//DPIの計算処理のマクロ
#define global ctype DPI_SIZ(%1) (%1*DPI/96)
#define global ctype SIZ_DPI(%1) (%1*96/DPI)

#define global ctype HIWORD(%1) (%1 >> 16 & $FFFF)

//旧バージョンのWindows10やそれ以前のOSではGetDpiForSystem関数が存在しないため、varptrでその関数が存在するかチェックし、存在しなければ別の関数でDPIの値を取得。
if varptr(GetDpiForSystem){DPI=GetDpiForSystem()}else{DPI=GetDeviceCaps(GetDC(0),LOGPIXELSY)}

screen 0,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/, , , ,DPI_SIZ(640),DPI_SIZ(480)

oncmd gosub *chgdpi,WM_DPICHANGED//DPI変更時の割り込み
title "高DPI対応テスト"
	cls
	font "Meiryo",DPI_SIZ(16)

	color 0,0,0
	mes "現在のDPI : " + DPI
	mes "倍率: "+int((double(DPI)/96)*100)+"%"
	mes "ディスプレイのサイズ : "+GetSystemMetricsForDpi(0,DPI)+"×"+GetSystemMetricsForDpi(1,DPI)+" ディスプレイスケーリング :"+SIZ_DPI(GetSystemMetricsForDpi(0,DPI))+"×"+SIZ_DPI(GetSystemMetricsForDpi(1,DPI))
	mes "\n640×480 : "+DPI_SIZ(640)+"×"+DPI_SIZ(480)+" スケーリング"
	mes "ウィンドウのクライアント領域のサイズ : "+ginfo_winx+"×"+ginfo_winy 
	mes "\nウィンドウ全体サイズ : " + ginfo(10)+"×"+ginfo(11)
	
	color 0,0,0
	mes "\nタイトルバーの高さ : " + GetSystemMetricsForDpi(SM_CYCAPTION,DPI)
	mes "小さなタイトルバーの高さ : " + GetSystemMetricsForDpi(SM_CYSMCAPTION,DPI)
	mes "サイズを変更できないウィンドウの枠の幅と高さ : " + GetSystemMetricsForDpi(SM_CXFIXEDFRAME,DPI) + ", " + GetSystemMetricsForDpi(SM_CYFIXEDFRAME,DPI)
	mes "サイズを変更できるウィンドウの枠の幅と高さ : " + GetSystemMetricsForDpi(SM_CXSIZEFRAME,DPI) + ", " + GetSystemMetricsForDpi(SM_CYSIZEFRAME,DPI)
stop
//DPI設定変更後の処理
*chgdpi
//wparamに新しいDPIが返ってくる
DPI = HIWORD(wparam)
//ウィンドウを表示する位置を取得（Rect構造体）
dupptr x_pos,lparam,  4 : dupptr y_pos,lparam+4,4
//dupptr x_size,  lparam+8,  4:dupptr y_size, lparam+12, 4//未使用

	WidthDPI DPI_SIZ(640),DPI_SIZ(480),x_pos,y_pos,DPI,1//ウィンドウのサイズの変更や位置を移動させる
 x_pos=0:y_pos=0
	
	cls
	font "Meiryo",DPI_SIZ(16)

	color 0,0,0
	mes "現在のDPI : " + DPI
	mes "倍率: "+int((double(DPI)/96)*100)+"%"
	mes "ディスプレイのサイズ : "+GetSystemMetricsForDpi(0,DPI)+"×"+GetSystemMetricsForDpi(1,DPI)+" ディスプレイスケーリング :"+SIZ_DPI(GetSystemMetricsForDpi(0,DPI))+"×"+SIZ_DPI(GetSystemMetricsForDpi(1,DPI))
	mes "\n640×480 : "+DPI_SIZ(640)+"×"+DPI_SIZ(480)+" スケーリング"
	mes "ウィンドウのクライアント領域のサイズ : "+ginfo_winx+"×"+ginfo_winy 
	mes "\nウィンドウ全体サイズ : " + ginfo(10)+"×"+ginfo(11)
	
	color 0,0,0
	mes "\nタイトルバーの高さ : " + GetSystemMetricsForDpi(SM_CYCAPTION,DPI)
	mes "小さなタイトルバーの高さ : " + GetSystemMetricsForDpi(SM_CYSMCAPTION,DPI)
	mes "サイズを変更できないウィンドウの枠の幅と高さ : " + GetSystemMetricsForDpi(SM_CXFIXEDFRAME,DPI) + ", " + GetSystemMetricsForDpi(SM_CYFIXEDFRAME,DPI)
	mes "サイズを変更できるウィンドウの枠の幅と高さ : " + GetSystemMetricsForDpi(SM_CXSIZEFRAME,DPI) + ", " + GetSystemMetricsForDpi(SM_CYSIZEFRAME,DPI)

return 0
//高DPIで正確なウィンドウサイズを変更する命令
//WidthDPI命令 wx 変更Xサイズ ,wy 変更Yサイズ , psx ウィンドウのX位置, psy ウィンドウのY位置 _DPI DPIの値（初期は96）, _opt  ウィンドウの表示位置オプション（0=位置を移動しない 1=位置を移動する）
#deffunc WidthDPI int wx,int wy,int psx ,int psy ,int _DPI,int _opt
	st = GetWindowLong(hwnd,-16)
	est=GetWindowLong(hwnd,-20)
	if wx >= 0 | wy >= 0{
		dim rect,4 : rect=0,0,wx,wy
		//旧バージョンのWindows10やそれ以前のOSではAdjustWindowRectExForDpi関数が存在しないため、varptrでその関数が存在するかチェックする。
		if varptr(AdjustWindowRectExForDpi){AdjustWindowRectExForDpi varptr(rect),st,0,est,_DPI}else{AdjustWindowRectEx varptr(rect),st,0,est}
		w=rect(2)-rect(0) : h=rect(3)-rect(1)
		SizeOpt = 0
	}else{
		SizeOpt =  SWP_NOSIZE
	}
	if _opt = 0:SizeOpt = SizeOpt | SWP_NOMOVE
	SetWindowPos hwnd,0,psx,psy,w,h,SizeOpt
return

#defcfunc GetSystemMetricsForDpi int p_1,int p_2
if varptr(_GetSystemMetricsForDpi){_GetSystemMetricsForDpi p_1,p_2}else{GetSystemMetrics p_1}
return stat
return
```
基本的に高DPIに対応させる方法として、[GetDeviceCaps関数](https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-getdevicecaps)でDPIを取得し、そのDPIを96を割ることで1ドットの単位が求められます。

今回はマクロで定義します。
```
DPI=GetDeviceCaps(GetDC(0),LOGPIXELSY)
#define global ctype DPI_SIZ(%1) (%1*DPI/96)
#define global ctype SIZ_DPI(%1) (%1*96/DPI)
```
## ソースコードの流れ
F5デバッグ時にuser32.dllの[SetProcessDpiAwarenessContext関数](SetProcessDpiAwarenessContext)を呼び出しDPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2を指定します。
どうやらこの関数は、Windows 10 バージョン1703以降らしい？
```hsp
opt = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2:SetProcessDpiAwarenessContext opt
```
そのあと、デバッグ時にマニフェストのファイルを保存します。

```
exist "DPI.manifest"
if strsize <= 0{
mani = {"
<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\" xmlns:asmv3=\"urn:schemas-microsoft-com:asm.v3\">
<assemblyIdentity
version=\"1.0.0.0\"
processorArchitecture=\"*\"
name=\"OnionSoftware.hsp3.exe\"
type=\"win32\"
/>

<application xmlns=\"urn:schemas-microsoft-com:asm.v3\">
<windowsSettings>
<dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>
<dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>

<description>XPStyle</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type=\"win32\"
name=\"Microsoft.Windows.Common-Controls\"
version=\"6.0.0.0\"
processorArchitecture=\"*\"
publicKeyToken=\"6595b64144ccf1df\"
language=\"*\"
/>
</dependentAssembly>
</dependency>
<compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">
  <application>
    <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />
    <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />
    <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />
    <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />
  </application>
</compatibility>
</assembly>
"}
notesel mani
notesave "DPI.manifest"
}
```
APIやマニフェストでDPI対応を宣言して、次にuser32.dllの[GetDpiForSystem関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getdpiforsystem)でDPIを取得します。
ただし、古いWindows10のバージョンやそれ以前のOSのバージョンではこの関数が存在しないため、gdi32.dllの[GetDeviceCaps関数](https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-getdevicecaps)で取得するようにしています。
```
//旧バージョンのWindows10やそれ以前のOSではGetDpiForSystem関数が存在しないため、varptrでその関数が存在するかチェックし、存在しなければ別の関数でDPIの値を取得。
if varptr(GetDpiForSystem){DPI=GetDpiForSystem()}else{DPI=GetDeviceCaps(GetDC(0),LOGPIXELSY)}
```
oncmdに[WM_DPICHANGED](https://learn.microsoft.com/ja-jp/windows/win32/hidpi/wm-dpichanged)を指定します。DPIが変更された際に割り込んできます。
wparamのHIWORDに新しいDPIが返ってきます。
lparamにも値が返ってくるので、dupptrでrect構造体を取得します。
```hsp
oncmd gosub *chgdpi,WM_DPICHANGED//DPI変更時の割り込み
```

```
DPI = HIWORD(wparam)
//ウィンドウを表示する位置を取得（Rect構造体）
dupptr x_pos,lparam,  4 : dupptr y_pos,lparam+4,4
//dupptr x_size,  lparam+8,  4:dupptr y_size, lparam+12, 4//未使用
```

新しいDPIとlparamのrect構造体に表示するウィンドウの位置を取得し、WidthDPIでウィンドウサイズを変更します。
流れとしては大体こんな感じです。

高DPIのWidthでは、正確にサイズが変更できないようなので、新たにWidthDPI命令というのを作っています。
```
#deffunc WidthDPI int wx,int wy,int psx ,int psy ,int _DPI,int _opt
	st = GetWindowLong(hwnd,-16)
	est=GetWindowLong(hwnd,-20)
	if wx >= 0 | wy >= 0{
		dim rect,4 : rect=0,0,wx,wy
		//旧バージョンのWindows10やそれ以前のOSではAdjustWindowRectExForDpi関数が存在しないため、varptrでその関数が存在するかチェックする。
		if varptr(AdjustWindowRectExForDpi){AdjustWindowRectExForDpi varptr(rect),st,0,est,_DPI}else{AdjustWindowRectEx varptr(rect),st,0,est}
		w=rect(2)-rect(0) : h=rect(3)-rect(1)
		SizeOpt = 0
	}else{
		SizeOpt =  SWP_NOSIZE
	}
	if _opt = 0:SizeOpt = SizeOpt | SWP_NOMOVE
	SetWindowPos hwnd,0,psx,psy,w,h,SizeOpt
return
```
WidthDPIは[AdjustWindowRectEx関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-adjustwindowrectex)のDPIに拡張した[AdjustWindowRectExForDpi関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-adjustwindowrectexfordpi)で正確なサイズを取得しています。

また、[GetSystemMetrics関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getsystemmetrics)をDPIに拡張した[GetSystemMetricsForDpi関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getsystemmetricsfordpi)でタスクバーの高さなどを取得しています。

[GetDpiForSystem関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getdpiforsystem)と同様、Windows10の古いバージョンやそれ以前のOSだと[AdjustWindowRectExForDpi関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-adjustwindowrectexfordpi)や[GetSystemMetricsForDpi関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getsystemmetricsfordpi)が存在しないようなのでvarptrでチェックしています。

## 注意点
この方法の注意点として、実行中にDPIを変更した場合、screenでウィンドウを初期化すると、Y（縦方向）のサイズがずれてしまうため、screenでウィンドウ初期化した後、ginfo_winyでYのサイズの誤差を求め、その後screenで初期化する必要があります。
```hsp
		screen 0,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/, screen_hide , , ,DPI_SIZ(640),DPI_SIZ(480)
		if DPI_SIZ(480) < ginfo_winy{
			gosa = ginfo_winy - DPI_SIZ(480)
			screen 0,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/, screen_hide , , ,DPI_SIZ(640),DPI_SIZ(480) - gosa
			dialog "Large"
		}if  DPI_SIZ(480) > ginfo_winy{
			gosa = DPI_SIZ(480) - ginfo_winy
			screen 0,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/, screen_hide , , ,DPI_SIZ(640),DPI_SIZ(480) + gosa
			dialog "Small"
		}
			/*WidthDPI DPI_SIZ(640),DPI_SIZ(480),0,0,DPI,0*/
		gsel 0,1
```
ぜひ参考になれば幸いです。

HSPで[WM_DPICHANGED](https://learn.microsoft.com/ja-jp/windows/win32/hidpi/wm-dpichanged)を使う方法の記事がなかったので書き込みました。

参考にした記事
[Windows ディスプレイ拡大率の反映と実DPIの取得](https://zenn.dev/tenka/articles/windows_display_monitor_dpi)