1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-10

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

Windows 8.1以降からモニターごとにDPIを設定できるようになりました。
HSPで高DPIに対応させるには、Windows APIのSetProcessDpiAwareを呼び出すか、マニフェストを書き換えるのが一般的のようですが、今回はDPIの設定の変更時やモニターごとのDPIに対応させる方法についてです。

DPI.hsp
#include "gdi32.as"
#include "user32.as"
#uselib "kernel32.dll"
#func Sleep "Sleep" int
#uselib "shcore.dll"
#func SetProcessDpiAwareness "SetProcessDpiAwareness" int

#uselib "user32.dll"
#ifndef SetProcessDpiAwarenessContext
#func global SetProcessDpiAwarenessContext "SetProcessDpiAwarenessContext" int
#endif

#ifndef SetProcessDPIAware
#func global SetProcessDPIAware "SetProcessDPIAware"
#endif

#ifndef AdjustWindowRectExForDpi
#func global AdjustWindowRectExForDpi "AdjustWindowRectExForDpi" int,int,int,int,int
#endif

#ifndef GetDpiForWindow
#cfunc global GetDpiForWindow "GetDpiForWindow" int
#endif
//#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

#define PROCESS_PER_MONITOR_DPI_AWARE 2
#define SM_CYCAPTION		0x00000004
#define SM_CXFIXEDFRAME	0x00000007
#define SM_CYFIXEDFRAME	0x00000008
#define SM_CXSIZEFRAME	0x00000020
#define SM_CYSIZEFRAME	0x00000021
#define SM_CYSMCAPTION	0x00000033

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

#packopt manifest "DPI.manifest"
#packopt hide 1
#ifdef _debug
#define W_ID 1
gsel 0,-1
//デバッグ時(F5実行)にもDPIを反映させる
if varptr(SetProcessDpiAwarenessContext){
	SetProcessDpiAwarenessContext DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
}else{
	if varptr(SetProcessDpiAwareness){
		SetProcessDpiAwareness PROCESS_PER_MONITOR_DPI_AWARE
	}else{
		if varptr(SetProcessDPIAware){SetProcessDPIAware}
	}
}
//デバッグ時(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"
}
#else
#define W_ID 0
#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ではGetDpiForWindow関数が存在しないため、varptrでその関数が存在するかチェックし、存在しなければ別の関数でDPIの値を取得。
if varptr(GetDpiForWindow){DPI=GetDpiForWindow(hwnd)}else{DPI=GetDeviceCaps(GetDC(0),LOGPIXELSY)}

screen W_ID,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/,screen_fixedsize, , ,DPI_SIZ(640),DPI_SIZ(480)
oncmd gosub *chgdpi,WM_DPICHANGED//DPI変更時の割り込み

gosa = 0
*main
title "高DPI対応テスト"
	cls
	font "Meiryo",DPI_SIZ(16)

	color 0,0,0
	mes "現在のDPI : " + DPI
	
	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)
	if gosa:mes "\nDPIの誤差"+gosa
	objmode 2
	objsize DPI_SIZ(200),DPI_SIZ((30)
	button "リセット",*reset
stop
*reset
		screen W_ID,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/,screen_hide | screen_fixedsize , , ,DPI_SIZ(640),DPI_SIZ(480)
		if ginfo_winy != DPI_SIZ(480){
			gosa = ginfo_winy - DPI_SIZ(480)
			screen W_ID,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/, screen_hide | screen_fixedsize , , ,DPI_SIZ(640),DPI_SIZ(480) - gosa
			/*WidthDPI DPI_SIZ(640),DPI_SIZ(480),0,0,DPI,0*/
		}
		gsel W_ID,1
		goto *main
//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//未使用

	wait 100
	Sleep 1000

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

	color 0,0,0
	mes "現在のDPI : " + DPI
	
	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)
	objmode 2
	objsize DPI_SIZ(200),DPI_SIZ((30)
	button "リセット",*reset
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

基本的に高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以降のようです。
SetProcessDpiAwarenessContextのAPIが存在しない場合は、shcore.dllのSetProcessDpiAwareness関数を呼び出します。

if varptr(SetProcessDpiAwarenessContext){
	SetProcessDpiAwarenessContext DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
}else{
	if varptr(SetProcessDpiAwareness){
		SetProcessDpiAwareness PROCESS_PER_MONITOR_DPI_AWARE
	}else{
		if varptr(SetProcessDPIAware){SetProcessDPIAware}
	}
}

そのあと、デバッグ時にマニフェストのファイルを保存します。

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のGetDpiForWindow関数でDPIを取得します。
ただし、古いWindows10のバージョンやそれ以前のOSのバージョンではこの関数が存在しないため、gdi32.dllのGetDeviceCaps関数で取得するようにしています。

//旧バージョンのWindows10やそれ以前のOSではGetDpiForWindow関数が存在しないため、varptrでその関数が存在するかチェックし、存在しなければ別の関数でDPIの値を取得。
if varptr(GetDpiForWindow){DPI=GetDpiForWindow(hwnd)}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関数でタスクバーの高さなどを取得しています。

GetDpiForWindow関数と同様、Windows10の古いバージョンやそれ以前のOSだとAdjustWindowRectExForDpi関数GetSystemMetricsForDpi関数が存在しないようなのでvarptrでチェックしています。

注意点

この方法の注意点として、実行中にDPIを変更した場合、screenでウィンドウを初期化すると、Y(縦方向)のサイズがずれてしまうため、screenでウィンドウ初期化した後、ginfo_winyでYのサイズの誤差を求め、その後screenで初期化する必要があります。

*reset
		screen W_ID,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/,screen_hide | screen_fixedsize , , ,DPI_SIZ(640),DPI_SIZ(480)
		if ginfo_winy != DPI_SIZ(480){
			gosa = ginfo_winy - DPI_SIZ(480)
			screen W_ID,640*8,480*8/*あらかじめ大きめの初期ウィンドウサイズを確保*/, screen_hide | screen_fixedsize , , ,DPI_SIZ(640),DPI_SIZ(480) - gosa
			/*WidthDPI DPI_SIZ(640),DPI_SIZ(480),0,0,DPI,0*/
		}
		gsel W_ID,1
goto *main

ぜひ参考になれば幸いです。

HSPでWM_DPICHANGEDを使う方法の記事がなかったので書き込みました。

参考にした記事
Windows ディスプレイ拡大率の反映と実DPIの取得

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?