HSP3Dish(HGIMG4)でスクリーンセーバーを作る

## はじまり

HSPにはスクリーンセーバーを作るサンプル `ssaver/arusave.hsp` が結構昔から同梱されており現在の最新バージョン（3.7β7）でもちゃんと動作するのですが、このサンプルは標準機能のみで作成されていて、DirectX/OpenGLを使用するHSP3Dish(HGIMG4)で単純に置き換えるとどうもプレビュー画面がうまくいきません。スクリーンセーバーの設定内にあるモニターに描画されなくて別のウィンドウが立ち上がりそこに描画されるのです。

スクリーンセーバーを作るには `#packopt type 2` というオプションがありますが、どうやらこれが標準機能だけにしか対応していない感じです。実行ファイル作成時の拡張子は `.scr` になりますがプレビュー画面には非対応なのでしょう。HSP3Dish(HGIMG4)はウィンドウは基本１つしか作れないのでサンプルにあるようなウィンドウID0とID2を使った作り方はできません。幸いID0を `screen` や `bgscr` で初期化すれば設定ウィンドウやフルスクリーンはできたので後はプレビュー画面を自力でなんとかすればいけそうだと思い作り始めました。ついでに高DPI環境やマルチモニターにまたがるスクリーンにも少し対応してみます。

![shot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151962/52e6eeeb-e4ad-d2bd-8a53-211e421c4c61.png)

## スクリプト

HSP 3.7β7、Windows 11 22H2 環境下でテストしています。

:::note warn
スクリプトはシフトJISで保存・実行してください。
:::

```dish_ss.hsp
; #use hgimg4
#use hsp3dish, gdi32, user32

#packopt name "dish_ss"
#packopt type 2
#packopt hide 1

#module

#uselib "user32"
#func GetDpiForWindow "GetDpiForWindow" int
#func SetProcessDPIAware "SetProcessDPIAware"
#uselib "shcore"
#func SetProcessDpiAwareness "SetProcessDpiAwareness" int

#deffunc setDpiAware
	if varptr(SetProcessDpiAwareness) {
		SetProcessDpiAwareness 2/*PROCESS_PER_MONITOR_DPI_AWARE*/
	} else : if varptr(SetProcessDPIAware) {
		SetProcessDPIAware
	}
	return

#defcfunc getDpi
	if (varptr(GetDpiForWindow)) {
		return GetDpiForWindow(hwnd)
	}
	return GetDeviceCaps(hdc, 88/*LOGPIXELSX*/)

#global


*start
	setDpiAware
	cmds = dir_cmdline
	randomize
	setreq SYSREQ_LOGWRITE, 0
	switch getpath(strmid(cmds, 1, 1), 16)
	case "s" : goto *main
	case "p" : goto *preview
	default  : goto *config
	swend
*bye
	end

*config
	scale = double(getDpi()) / 96 ; 画面の倍率を計算
	scrw = int(scale * 320)
	scrh = int(scale * 240)
	screen 0, scrw, scrh
	title "設定ウィンドウ（仮"
	gosub *initDraw
	font "メイリオ", scale * 24, 17
	objmode objmode_usefont
	pos scale * 240, scale * 200
	objsize scale * 64, scale * 24
	button "OK", *bye
	repeat
		redraw 0
		gosub *drawBg
		pos scale * 10, scale * 10
		rgbcolor $ffffff
		mes "設定をファイルに保存するのです", mesopt_shadow
		redraw 1
		await 30
	loop

*preview
	getstr ts, cmds, 3 ; コマンドライン "/p xxxxx" xxxxxの部分がプレビューウィンドウのハンドル
	pwin = int(ts)
	dim rc, 4 ; RECT
	GetWindowRect pwin, varptr(rc)
	scrw = rc(2) - rc(0)
	scrh = rc(3) - rc(1)
	bgscr 0, scrw, scrh, screen_hide, rc(0), rc(1)
	if (GetWindow(pwin, 5/*GW_CHILD*/)) : goto *bye ; 先客がいたら終了する
	SetWindowLong hwnd, -16/*GWL_STYLE*/, $56000000/*WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS|WS_VISIBLE*/
	SetParent hwnd, pwin
	MoveWindow hwnd, 0, 0, scrw, scrh, 0
	gosub *initDraw
	repeat
		redraw 0
		gosub *drawBg
		redraw 1
		await 30
	loop

*main
	scrw = GetSystemMetrics($4e/*SM_CXVIRTUALSCREEN*/)
	scrh = GetSystemMetrics($4f/*SM_CYVIRTUALSCREEN*/)
	bgscr 0, scrw, scrh, , GetSystemMetrics($4c/*SM_XVIRTUALSCREEN*/), GetSystemMetrics($4d/*SM_YVIRTUALSCREEN*/)
	oncmd gosub *chkDown, $100 ; WM_KEYDOWN
	oncmd gosub *chkDown, $201 ; WM_LBUTTONDOWN
	oncmd gosub *chkDown, $204 ; WM_RBUTTONDOWN
	oncmd gosub *chkDown, $207 ; WM_MBUTTONDOWN
	oncmd gosub *chkDown, $20b ; WM_XBUTTONDOWN
	; gsel 0, 2 ; 非対応？
	SetWindowPos hwnd, -1/*HWND_TOPMOST*/, 0, 0, 0, 0, $3/*SWP_NOMOVE|SWP_NOSIZE*/
	mouse -1
	fcnt = 0
	gosub *initDraw
	repeat
		gosub *chkMouseMove
		redraw 0
		gosub *drawBg
		redraw 1
		await 30
		fcnt++
	loop

*chkDown
	if (fcnt > 10) : goto *bye
	return 0
*chkMouseMove
	; WM_MOUSEMOVEは飛んでき過ぎるので自力で…
	if (fcnt > 10) {
		if (mousex != mpx || mousey != mpy) : goto *bye
	}
	mpx = mousex
	mpy = mousey
	return

#module mvObj px, py, vx, vy, rot, sz, col

#modinit double _x, double _y, double _s, double _a
	px = _x
	py = _y
	vx = cos(_a) * _s / 150
	vy = sin(_a) * _s / 150
	col = rnd(192)
	sz = _s / 3
	rot = 0.01 * rnd(314)
	return

#modfunc drawObj double _w, double _h
	col = (col + 1) \ 192
	rot += 0.01
	if (px < 0) : px = -px : vx *= -1
	if (py < 0) : py = -py : vy *= -1
	if (px > _w) : px = _w * 2 - px : vx *= -1
	if (py > _h) : py = _h * 2 - py : vy *= -1
	px += vx
	py += vy
	gmode 5, sz, sz, 8
	hsvcolor col, 100, 200
	grect px, py, rot
	return

#global

*initDraw
	scx = scrw / 2
	scy = scrh / 2
	if (scrw > scrh) {
		bs = scrh
	} else {
		bs = scrw
	}
	num = 5 * scrw * scrh / (bs * bs)
	a = 0.01 * rnd(628)
	p = 6.28 / num
	repeat num
		a += p
		newmod obj, mvObj, scx, scy, bs, a
	loop
	setcls 1 : redraw 0
	setcls 0
	return

*drawBg
	gmode 3, scrw, scrh, 8
	color
	grect scx, scy
	foreach obj
		drawObj obj(cnt), scrw, scrh
	loop
	return
```

以下、部分的に解説等いれていきます。

## インクルード・自動作成オプション

```
; #use hgimg4
#use hsp3dish, gdi32, user32

#packopt name "dish_ss"
#packopt type 2
#packopt hide 1
```

`#use` は HSP 3.7β7 から導入されたプリプロセッサ命令で `#include` を簡潔に書けるようになった命令です。`; #use hgimg4` をコメント解除するとHGIMG4を使って実行します。`hgimg4` と `hsp3dish` は同時に指定可能ですがその場合は `hgimg4` を先に指定しないとエラーになります。

今回 `#packopt type 2` は実行ファイル作成時に拡張子を `.scr` にするためだけに指定しています。指定しない場合は `.exe` になりますが手動で `.scr` にリネームしても同様のスクリーンセーバーになります。

## 高DPI対応

```
#module

#uselib "user32"
#func GetDpiForWindow "GetDpiForWindow" int
#func SetProcessDPIAware "SetProcessDPIAware"
#uselib "shcore"
#func SetProcessDpiAwareness "SetProcessDpiAwareness" int

#deffunc setDpiAware
	if varptr(SetProcessDpiAwareness) {
		SetProcessDpiAwareness 2/*PROCESS_PER_MONITOR_DPI_AWARE*/
	} else : if varptr(SetProcessDPIAware) {
		SetProcessDPIAware
	}
	return

#defcfunc getDpi
	if (varptr(GetDpiForWindow)) {
		return GetDpiForWindow(hwnd)
	}
	return GetDeviceCaps(hdc, 88/*LOGPIXELSX*/)

#global
```

マルチモニターを覆うスクリーンである仮想画面のサイズ計算する手間が省ける他に、設定画面のクッキリ化のためのモジュールです。詳細は省きますが、「このアプリは高DPI対応してるよ！」と宣言することで一部のAPIがDPIに応じた値を返してくれたり、画面拡大時にHSPでは通常ぼやける表示をクッキリ描画できるようにします。

## 起動後の初期処理・終了処理

```
*start
	setDpiAware
	cmds = dir_cmdline
	randomize
	setreq SYSREQ_LOGWRITE, 0
	switch getpath(strmid(cmds, 1, 1), 16)
	case "s" : goto *main
	case "p" : goto *preview
	default  : goto *config
	swend
*bye
	end
```

`getpath(strmid(cmds, 1, 1), 16)` でコマンドライン文字列の２文字目を小文字化、`switch` で文字毎に場合分けしています。ここはサンプルとやっていることは同じです。

終了処理、始めは `onexit` で飛ばしていたのですが、終了時にエラーが出るなどで泥沼にハマったので止めました。

## 設定画面

```
*config
	scale = double(getDpi()) / 96 ; 画面の倍率を計算
	scrw = int(scale * 320)
	scrh = int(scale * 240)
	screen 0, scrw, scrh
	title "設定ウィンドウ（仮"
	gosub *initDraw
	font "メイリオ", scale * 24, 17
	objmode objmode_usefont
	pos scale * 240, scale * 200
	objsize scale * 64, scale * 24
	button "OK", *bye
	repeat
		redraw 0
		gosub *drawBg
		pos scale * 10, scale * 10
		rgbcolor $ffffff
		mes "設定をファイルに保存するのです", mesopt_shadow
		redraw 1
		await 30
	loop
```

高DPI対応宣言した場合は自力でウィンドウやフォントのサイズや位置などを計算しないといけないので、倍率を取得してそれぞれの値に掛けていきます。

## プレビュー画面

```
*preview
	getstr ts, cmds, 3 ; コマンドライン "/p xxxxx" xxxxxの部分がプレビューウィンドウのハンドル
	pwin = int(ts)
	dim rc, 4 ; RECT
	GetWindowRect pwin, varptr(rc)
	scrw = rc(2) - rc(0)
	scrh = rc(3) - rc(1)
	bgscr 0, scrw, scrh, screen_hide, rc(0), rc(1)
	if (GetWindow(pwin, 5/*GW_CHILD*/)) : goto *bye ; 先客がいたら終了する
	SetWindowLong hwnd, -16/*GWL_STYLE*/, $56000000/*WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS|WS_VISIBLE*/
	SetParent hwnd, pwin
	MoveWindow hwnd, 0, 0, scrw, scrh, 0
	gosub *initDraw
	repeat
		redraw 0
		gosub *drawBg
		redraw 1
		await 30
	loop
```

今回のメインとなる箇所です。コメントにも書いてありますがコマンドライン文字列にプレビューウィンドウのハンドルが載っているので、そのハンドルからウィンドウの位置サイズを取得して `bgscr` でスクリーンを初期化、ウィンドウスタイルを変更してプレビューウィンドウの子ウィンドウにする、という流れです。

ウィンドウスタイルを変更するのは子ウィンドウにするためで、HSPのウィンドウには `WS_POPUP` が含まれており子ウィンドウにするための `WS_CHILD` とは共存できないからです。

`bgscr` の第４パラメータに `screen_hide` を指定していますが現バージョンではどうも無効になっているようなので、プレビューウィンドウを覆う形で初期化して誤魔化しています。本当は初めは非表示にしておいて準備できたら表示したいところです。

途中にある先客がいたら～の箇所ですが、プレビューを高速で切り替えると前のプレビューが残ったまま次のプレビューと点滅しながら表示されることがあり、HSP側でうまく終了できていないようなので無理やりですが入れてみました。自分の環境ではうまく終了してくれますがちょっと怪しい感じはします…

## フルスクリーン画面

```
*main
	scrw = GetSystemMetrics($4e/*SM_CXVIRTUALSCREEN*/)
	scrh = GetSystemMetrics($4f/*SM_CYVIRTUALSCREEN*/)
	bgscr 0, scrw, scrh, , GetSystemMetrics($4c/*SM_XVIRTUALSCREEN*/), GetSystemMetrics($4d/*SM_YVIRTUALSCREEN*/)
	oncmd gosub *chkDown, $100 ; WM_KEYDOWN
	oncmd gosub *chkDown, $201 ; WM_LBUTTONDOWN
	oncmd gosub *chkDown, $204 ; WM_RBUTTONDOWN
	oncmd gosub *chkDown, $207 ; WM_MBUTTONDOWN
	oncmd gosub *chkDown, $20b ; WM_XBUTTONDOWN
	; gsel 0, 2 ; 非対応？
	SetWindowPos hwnd, -1/*HWND_TOPMOST*/, 0, 0, 0, 0, $3/*SWP_NOMOVE|SWP_NOSIZE*/
	mouse -1
	fcnt = 0
	gosub *initDraw
	repeat
		gosub *chkMouseMove
		redraw 0
		gosub *drawBg
		redraw 1
		await 30
		fcnt++
	loop

*chkDown
	if (fcnt > 10) : goto *bye
	return 0
*chkMouseMove
	; WM_MOUSEMOVEは飛んでき過ぎるので自力で…
	if (fcnt > 10) {
		if (mousex != mpx || mousey != mpy) : goto *bye
	}
	mpx = mousex
	mpy = mousey
	return
```

`GetSystemMetrics` を使ってマルチモニターにまたがるスクリーンの位置サイズを取得しています。高DPI対応宣言しているとDPIに応じた値を返してくれます。

`oncmd` でキーやマウスの押下メッセージを取って終了しています。マウスカーソルの移動だけは自分の環境だと何故か動いてないときも `WM_MOUSEMOVE` がすごい勢いで飛んでくるので、ループ内で自力で処理しています。標準機能で作ったスクリーンセーバーだとこの辺の処理も内部でやってくれています。`if (fcnt > 10)` である程度猶予をもたせています。起動した瞬間終了すると起動したのか分からないので。

スクリーンの最前面にもってくるため `gsel 0, 2` としたかったのですが第２パラメータが非対応っぽいので代わりに `SetWindowPos` を使っています。

## 描画部分

`#module mvObj` 以下の部分は、回転する四角形の描画部分です。画面端で跳ね返ったり色が徐々に変わったり。画面の大きさによって四角形のサイズや数を変えています。

## おわり

モニターが液晶になってからスクリーンセーバーの役目も無くなりつつあるのかもしれません。HSPもこの辺の対応がされていないのもその影響なのかな、と思ったり思わなかったり…