HSPでDPI変更時やモニターごとに高DPIに対応させる方法について
HSPで高DPIに対応させるには、Windows APIのSetProcessDpiAwareを呼び出すか、マニフェストを書き換えるのが一般的のようですが、今回はDPIの設定の変更時やモニターごとのDPIに対応させる方法についてです。
#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関数で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関数を呼び出しDPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2を指定します。
どうやらこの関数は、Windows 10 バージョン1703以降らしい?
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関数でDPIを取得します。
ただし、古いWindows10のバージョンやそれ以前のOSのバージョンではこの関数が存在しないため、gdi32.dllのGetDeviceCaps関数で取得するようにしています。
//旧バージョンのWindows10やそれ以前のOSではGetDpiForSystem関数が存在しないため、varptrでその関数が存在するかチェックし、存在しなければ別の関数でDPIの値を取得。
if varptr(GetDpiForSystem){DPI=GetDpiForSystem()}else{DPI=GetDeviceCaps(GetDC(0),LOGPIXELSY)}
oncmdにWM_DPICHANGEDを指定します。DPIが変更された際に割り込んできます。
wparamのHIWORDに新しいDPIが返ってきます。
lparamにも値が返ってくるので、dupptrでrect構造体を取得します。
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関数のDPIに拡張したAdjustWindowRectExForDpi関数で正確なサイズを取得しています。
また、GetSystemMetrics関数をDPIに拡張したGetSystemMetricsForDpi関数でタスクバーの高さなどを取得しています。
GetDpiForSystem関数と同様、Windows10の古いバージョンやそれ以前のOSだとAdjustWindowRectExForDpi関数やGetSystemMetricsForDpi関数が存在しないようなのでvarptrでチェックしています。
注意点
この方法の注意点として、実行中にDPIを変更した場合、screenでウィンドウを初期化すると、Y(縦方向)のサイズがずれてしまうため、screenでウィンドウ初期化した後、ginfo_winyでYのサイズの誤差を求め、その後screenで初期化する必要があります。
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を使う方法の記事がなかったので書き込みました。
参考にした記事
Windows ディスプレイ拡大率の反映と実DPIの取得