[HSP]ウィンドウとフルスクリーンを切り替える

# はじめに
ゲームなどで使えるウィンドウモードとフルスクリーンモードを切り替える処理のサンプルです。マルチディスプレイ環境下でもそれぞれのディスプレイでフルスクリーンになります。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151962/9a9eb380-4826-d760-356e-7eecbc05b456.gif)
サンプルはフルスクリーン化の方法別に以下の2パターンを書いています。

- スクリーン（ウィンドウのクライアント部）の大きさは変えずに（スタイルは変える）、ディスプレイの解像度をスクリーンの大きさに合わせる
- ディスプレイの解像度は変えずに、スクリーンの大きさを解像度に合わせる

どちらもメインディスプレイのみであれば標準命令のみで対応可能ですが、マルチ環境下でサブ画面にフルスクリーンにするには標準命令だけでは現状無理なので Windows API を使っています。またモード切り替えの際に screen や bgscr でウィンドウを作り直すのではなく、ウィンドウスタイルやサイズを変えるためにも Windows API を使います。その分コード量は増えますが処理の最適化やメモリの節約に繋がっていると思います。

それぞのサンプルは通常HSP版とHGIMG4版での両方で動くようになっています。大体同じ挙動になるようにしていますが、HSP3.6β1以前のHGIMG4では oncmd に未対応なのでその部分は通常HSP版と動作が異なります。サンプルはモード切替に関わる処理はモジュールとして分けてあるので、既存のスクリプトにも組み込み易くなっていると思います。

サンプルはいずれも HSP3.6β1~2, Windows 10 ver.1903 で動作確認しています。HSP3.6β2用に直したスクリプトを下記URLに置いています。
https://gist.github.com/skymonsters-Ks/ccea0d954992cc5be23672ce72931593 

# ディスプレイ解像度変更版

スクリーンの大きさは変えずに、ディスプレイの解像度をスクリーンの大きさに合わせる方法です。スクリーンサイズ自体は変わらないのでもう一つの方法に比べて描画負荷は小さくなると思います。ただしディスプレイやビデオデバイスに依って対応解像度が決まっているので、どんなサイズでもフルスクリーンにできるわけではないことに注意が必要です。

```fw_test1.hsp
; #include "hgimg4.as"
#include "user32.as"

#module

#const GWL_STYLE   -16
#const WS_POPUP    $80000000
#const WS_VISIBLE  $10000000
#const WM_KILLFOCUS        $8
#const WM_GETMINMAXINFO    $24
#const WM_WINDOWPOSCHANGED $47
#const SM_CMONITORS        80
#const HWND_NOTOPMOST      -2
#const HWND_TOPMOST        -1
#const SWP_FRAMECHANGED    $20
#const SWP_SHOWWINDOW      $40
#const CDS_TEST            $2
#const CDS_FULLSCREEN      $4
#const ENUM_CURRENT_SETTINGS -1
#const MONITOR_DEFAULTTONEAREST 2

; モジュール初期化
; 対象ウィンドウが選択された状態で呼び出して
; 内で oncmd いくつか使っているので重複注意
#deffunc fw_init
	dim rect, 4 ; RECT
	dim dmode, 40 ; DEVMODE
	dim minfo, 18 ; MONITORINFOEX
	minfo = 72 ; cbSize
	full = 0
	getWinSize mmw, mmh
	oncmd gosub *wmGetMinMaxInfo, WM_GETMINMAXINFO
	oncmd gosub *wmWindowPosChanged, WM_WINDOWPOSCHANGED
	oncmd gosub *wmKillFocus, WM_KILLFOCUS
	return

; ウィンドウサイズ上限解除とモニター間移動時のリサイズ防止
*wmGetMinMaxInfo
	dupptr mmi, lparam + 24, 16 ; MINMAXINFO ptMinTrackSize, ptMaxTrackSize
	mmi = mmw, mmh, mmw, mmh
	return 0

; フルスクリーンモードでウィンドウ移動が起こったとき用
; 本当はモニター間移動時のメッセージとかがほしい…
*wmWindowPosChanged
	if full {
		dupptr wp, lparam, 28 ; WINDOWPOS
		if wp(2) != monix || wp(3) != moniy {
			winx = wp(2)
			winy = wp(3)
			fw_change 0
			fw_change 1
		}
	}
	return 0

; フルスクリーンモードでフォーカス失ったとき用
; シングルモニタ環境下ではフルスクリーン解除した方がいいと思う
*wmKillFocus
	if full : if GetSystemMetrics(SM_CMONITORS) == 1 : fw_change 0
	return 0

; モード変更。0 でウィンドウモード、1 でフルスクリーンモードに。stat 0 で変更成功、1 で失敗。
#deffunc fw_change int _m, local _mh
	if _m {
		if full == 1 : return 1
		getWinPos winx, winy
		getWinSize winw, winh
		winstyle = GetWindowLong(hwnd, GWL_STYLE)
		_mh = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)
		mname = getMoniName(_mh)
		chgDispEx mname, ginfo_sx, ginfo_sy
		if stat : return 1
		mmw = ginfo_sx
		mmh = ginfo_sy
		getMoniPos monix, moniy, _mh
		chgWin HWND_TOPMOST, monix, moniy, mmw, mmh, WS_POPUP | WS_VISIBLE
		full = 1
	} else {
		if full == 0 : return 1
		mmw = winw
		mmh = winh
		full = 0 ; 直後の chgDispEx, chgWin で何度かウィンドウメッセージが来て wmWindowPosChanged を通過するので先に変更しておく
		chgDispEx mname
		chgWin HWND_NOTOPMOST, winx, winy, winw, winh, winstyle
	}
	return 0

; モード取得。0 ウィンドウモード、1 フルスクリーンモード
#defcfunc fw_stat
	return full

; ginfo_wx1(wy1) と同じ(未対応HGIMG4用)
#deffunc local getWinPos var _x, var _y
	GetWindowRect hwnd, varptr(rect)
	_x = rect(0)
	_y = rect(1)
	return

; ginfo_sizex(sizey) と同じ(未対応HGIMG4用)
#deffunc local getWinSize var _w, var _h
	GetWindowRect hwnd, varptr(rect)
	_w = rect(2) - rect(0)
	_h = rect(3) - rect(1)
	return

#deffunc local chgWin int _z, int _x, int _y, int _w, int _h, int _s
	SetWindowLong hwnd, GWL_STYLE, _s
	SetWindowPos hwnd, _z, _x, _y, _w, _h, SWP_FRAMECHANGED | SWP_SHOWWINDOW
	return

#defcfunc local getMoniName int _mh, local _mn
	GetMonitorInfo _mh, varptr(minfo)
	getstr _mn, minfo, 40 ; szDevice
	return _mn

#deffunc local getMoniPos var _x, var _y, int _mh
	GetMonitorInfo _mh, varptr(minfo)
	_x = minfo(1)
	_y = minfo(2)
	return

; 指定デバイスモードが対応しているかを取得。0 対応、1 未対応
#defcfunc local chkDisp str _name, int _w, int _h, int _fr
	EnumDisplaySettings _name, ENUM_CURRENT_SETTINGS, varptr(dmode)
	dmode(27) = _w ; dmPelsWidth
	dmode(28) = _h ; dmPelsHeight
	if _fr > 0 {
		dmode(30) = _fr ; dmDisplayFrequency
	}
	ChangeDisplaySettingsEx _name, varptr(dmode), 0, CDS_TEST, 0
	return stat < 0

; デバイスモード変更。_w に 0 指定でモードを戻す。stat 0 で成功、1 で失敗
#deffunc local chgDispEx str _name, int _w, int _h, int _fr
	if _w > 0 {
		if chkDisp(_name, _w, _h, _fr) : return 1
		ChangeDisplaySettingsEx _name, varptr(dmode), 0, CDS_FULLSCREEN, 0
	} else {
		ChangeDisplaySettingsEx _name, 0, 0, CDS_FULLSCREEN, 0
	}
	return 0

#global


	; screen 0, 800, 600
#ifdef _HGIMG4
	gpreset
#endif
	fw_init
	sx = ginfo_sx
	sy = ginfo_sy
	px = sx / 2
	py = sy / 2
	vx = 3
	vy = 3
	sz = 200
	szh = sz / 2

*mainLoop
	gosub *checkKey
	gosub *move
	gosub *draw
	await 15
	goto *mainLoop

*checkKey
	stick key
	if key & 128 : end ; [Esc]
	if key & 32 {      ; [Enter]
		fw_change fw_stat() ^ 1
		if stat {
			ffc = 200
		}
	}
	return

*move
	px += vx
	py += vy
	if px < szh : px = sz - px : vx = -vx
	if py < szh : py = sz - py : vy = -vy
	if px > sx - szh : px = 2 * (sx - szh) - px : vx = -vx
	if py > sy - szh : py = 2 * (sy - szh) - py : vy = -vy
	return

*draw
	redraw 0
	color 50, 0, 100
	boxf
	color 250, 50, 150
	grect px, py, 0, sz, sz
	line rnd(sx), sy, rnd(sx), 0
	pos 10, 10
	color 200, 200, 200
	if fw_stat() {
		mes "Fullscreen Mode"
	} else {
		mes "Window Mode"
	}
	if ffc {
		mes strf("[%dx%d] incompatible mode", sx, sy)
		ffc--
	}
	redraw 1
	return
```

# ウィンドウ拡張版

ディスプレイの解像度は変えずに、スクリーンの大きさを解像度に合わせる方法です。もう一つの方法と比べどんなサイズでもフルスクリーンにできますが、バックバッファをメインウィンドウに拡縮コピーするという処理なので描画負荷は高めになると思います。こちらは処理上利用しやすいのでウィンドウモード時のリサイズ機能も付けてみました。

```fw_test2.hsp
; #include "hgimg4.as"
#include "user32.as"
#include "gdi32.as"

#module

#const GWL_STYLE   -16
#const BLACK_BRUSH 4
#const WS_POPUP    $80000000
#const WS_VISIBLE  $10000000
#const SRCCOPY     $00cc0020
#const WM_KILLFOCUS        $8
#const WM_GETMINMAXINFO    $24
#const WM_WINDOWPOSCHANGED $47
#const SM_CMONITORS        80
#const HWND_NOTOPMOST      -2
#const HWND_TOPMOST        -1
#const SWP_FRAMECHANGED    $20
#const SWP_SHOWWINDOW      $40
#const MONITOR_DEFAULTTONEAREST 2

; モジュール初期化
; 対象ウィンドウが選択された状態で呼び出して
; 内で oncmd いくつか使っているので重複注意
; HGIMG4 使用時は _id にモジュール内で使うウィンドウID指定可、省略すると呼び出し時点での未使用IDを使用、stat にIDが返る
#deffunc fw_init int _id
	dim point, 2 ; POINT
	dim rect, 4 ; RECT
	dim minfo, 18 ; MONITORINFOEX
	minfo = 72 ; cbSize
	rate = 100
	full = 0
	destw = ginfo_sx
	desth = ginfo_sy
	getWinSize mmw, mmh
	ncw = mmw - destw
	nch = mmh - desth
#ifdef _HGIMG4
	winid = ginfo_sel
	if _id <= 0 {
		bufid = ginfo_newid
	} else {
		bufid = _id
	}
	buffer bufid, destw, desth, screen_offscreen
	gsel winid
	setcls 1, $000000
#else
	bufid = ginfo_sel
#endif
	oncmd gosub *wmGetMinMaxInfo, WM_GETMINMAXINFO
	oncmd gosub *wmWindowPosChanged, WM_WINDOWPOSCHANGED
	oncmd gosub *wmKillFocus, WM_KILLFOCUS
	return bufid

; ウィンドウサイズ上限解除とモニター間移動時のリサイズ防止
*wmGetMinMaxInfo
	dupptr mmi, lparam + 24, 16 ; MINMAXINFO ptMinTrackSize, ptMaxTrackSize
	mmi = mmw, mmh, mmw, mmh
	return 0

; フルスクリーンモードでウィンドウ移動が起こったとき用
; 本当はモニター間移動時のメッセージとかがほしい…
*wmWindowPosChanged
	if full {
		dupptr wp, lparam, 28 ; WINDOWPOS
		if wp(2) != monix || wp(3) != moniy {
			winx = wp(2)
			winy = wp(3)
			fw_change 0
			fw_change 1
		}
	}
	return 0

; フルスクリーンモードでフォーカス失ったとき用
; シングルモニタ環境下ではフルスクリーン解除した方がいいと思う
*wmKillFocus
	if full : if GetSystemMetrics(SM_CMONITORS) == 1 : fw_change 0
	return 0

; redraw の代わりに使用する、パラメータ省略時は挙動が違うので注意
#deffunc fw_redraw int _m, local _dc
#ifdef _HGIMG4
	if full || rate != 100 {
		if _m {
			redraw 1
			gsel winid
			redraw 0
			gpviewport destx, desty, destw, desth
			gmode 0
			gmulcolor
			pos 0, 0
			celput bufid
			redraw 1
		} else {
			gsel bufid
			redraw 0
			gpviewport 0, 0, ginfo_sx, ginfo_sy
			; gfilter 0
		}
	} else {
		redraw _m
		gpviewport 0, 0, ginfo_sx, ginfo_sy
	}
#else
	if full || rate != 100 {
		if _m {
			_dc = GetDC(hwnd)
			StretchBlt _dc, destx, desty, destw, desth, hdc, 0, 0, ginfo_sx, ginfo_sy, SRCCOPY
			ReleaseDC hwnd, _dc
		} else {
			redraw 0
		}
	} else {
		redraw _m
	}
#endif
	return

; モード変更。0 でウィンドウモード、1 でフルスクリーンモードに
#deffunc fw_change int _m, local _mh, local _dc
	if _m {
		if full == 1 : return
		getWinPos winx, winy
		getWinSize winw, winh
		winstyle = GetWindowLong(hwnd, GWL_STYLE)
		_mh = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)
		getMoniPos monix, moniy, _mh
		getMoniSize mmw, mmh, _mh
		; スクリーンを中央に配置するための計算
		if mmw * ginfo_sy < mmh * ginfo_sx {
			destw = mmw
			desth = mmw * ginfo_sy / ginfo_sx
			destx = 0
			desty = (mmh - desth) / 2
		} else {
			destw = mmh * ginfo_sx / ginfo_sy
			desth = mmh
			destx = (mmw - destw) / 2
			desty = 0
		}
		chgWin HWND_TOPMOST, monix, moniy, mmw, mmh, WS_POPUP | WS_VISIBLE
		full = 1
	} else {
		if full == 0 : return
		destx = 0
		desty = 0
		destw = ginfo_sx * rate / 100
		desth = ginfo_sy * rate / 100
		mmw = winw
		mmh = winh
		full = 0 ; 直後の chgWin でウィンドウメッセージが来て wmWindowPosChanged を通過するので先に変更しておく
		chgWin HWND_NOTOPMOST, winx, winy, winw, winh, winstyle
	}
#ifndef _HGIMG4
	; '画面外' の塗りつぶしとHSPバッファのちらつき低減
	ValidateRect hwnd, 0
	color : boxf : redraw 1
	_dc = GetDC(hwnd)
	SelectObject _dc, GetStockObject(BLACK_BRUSH)
	Rectangle _dc, 0, 0, mmw, mmh
	ReleaseDC hwnd, _dc
#endif
	return

; スクリーンリサイズ、_r は％指定、100 でオリジナル倍率
#deffunc fw_resize int _r
	if full == 0 {
		if _r < 10 {
			rate = 10
		} else {
			rate = _r
		}
		destx = 0
		desty = 0
		destw = ginfo_sx * rate / 100
		desth = ginfo_sy * rate / 100
		mmw = destw + ncw
		mmh = desth + nch
		getWinPos winx, winy
		chgWin HWND_NOTOPMOST, winx, winy, mmw, mmh, GetWindowLong(hwnd, GWL_STYLE)
#ifndef _HGIMG4
		ValidateRect hwnd, 0
		color : boxf : redraw 1
#endif
	}
	return

; 状態取得
#defcfunc fw_stat int _m
	switch _m
	case 1 : return rate  ; スクリーン倍率
	case 2 : return destw ; スクリーン幅
	case 3 : return desth ; スクリーン高さ
	swend
	return full ; 0 ウィンドウモード、1 フルスクリーンモード

#defcfunc fw_mousex
	GetCursorPos varptr(point)
	ScreenToClient hwnd, varptr(point)
	return limit((point(0) - destx) * ginfo_sx / destw, 0, ginfo_sx - 1)

#defcfunc fw_mousey
	GetCursorPos varptr(point)
	ScreenToClient hwnd, varptr(point)
	return limit((point(1) - desty) * ginfo_sy / desth, 0, ginfo_sy - 1)

#undef mousex
#define global mousex fw_mousex()
#undef mousey
#define global mousey fw_mousey()

; ginfo_wx1(wy1) と同じ(未対応HGIMG4用)
#deffunc local getWinPos var _x, var _y
	GetWindowRect hwnd, varptr(rect)
	_x = rect(0)
	_y = rect(1)
	return

; ginfo_sizex(sizey) と同じ(未対応HGIMG4用)
#deffunc local getWinSize var _w, var _h
	GetWindowRect hwnd, varptr(rect)
	_w = rect(2) - rect(0)
	_h = rect(3) - rect(1)
	return

#deffunc local chgWin int _z, int _x, int _y, int _w, int _h, int _s
	SetWindowLong hwnd, GWL_STYLE, _s
	SetWindowPos hwnd, _z, _x, _y, _w, _h, SWP_FRAMECHANGED | SWP_SHOWWINDOW
	return

#deffunc local getMoniPos var _x, var _y, int _mh
	GetMonitorInfo _mh, varptr(minfo)
	_x = minfo(1)
	_y = minfo(2)
	return

#deffunc local getMoniSize var _w, var _h, int _mh
	GetMonitorInfo _mh, varptr(minfo)
	_w = minfo(3) - minfo(1)
	_h = minfo(4) - minfo(2)
	return

#global


	; screen 0, 256, 240
#ifdef _HGIMG4
	gpreset
#endif
	fw_init ; HGIMG4 使用時は buffer, celload など他で使用しない0以外のウィンドウIDを指定する
	sx = ginfo_sx
	sy = ginfo_sy
	px = sx / 2
	py = sy / 2
	vx = 3
	vy = 3
	sz = 200
	szh = sz / 2

*mainLoop
	gosub *checkKey
	gosub *move
	gosub *draw
	await 15
	goto *mainLoop

*checkKey
	stick key
	if key & 128 : end       ; [Esc]
	if key & 32  : fw_change fw_stat() ^ 1 ; [Enter]
	if key & 1 : fw_resize fw_stat(1) - 10  ; [left]
	if key & 4 : fw_resize fw_stat(1) + 10  ; [right]
	if key & 2 : fw_resize fw_stat(1) + 100 ; [up]
	if key & 8 : fw_resize fw_stat(1) - 100 ; [down]
	return

*move
	px += vx
	py += vy
	if px < szh : px = sz - px : vx = -vx
	if py < szh : py = sz - py : vy = -vy
	if px > sx - szh : px = 2 * (sx - szh) - px : vx = -vx
	if py > sy - szh : py = 2 * (sy - szh) - py : vy = -vy
	return

*draw
	fw_redraw 0
	color 50, 0, 100
	boxf
	color 250, 50, 150
	grect px, py, 0, sz, sz
	line rnd(sx), sy, rnd(sx), 0
	pos 10, 10
	color 200, 200, 200
	if fw_stat() {
		mes "Fullscreen Mode"
	} else {
		mes "Window Mode"
		mes strf("Size: %dx%d (%d%%)", fw_stat(2), fw_stat(3), fw_stat(1))
	}
	mes strf("Mouse X:%d, Y:%d", mousex, mousey)
	fw_redraw 1
	return
```

# おわりに

どちらの方法も一長一短ありますが、個人的には2つ目の方法がいいと思っています。理由はどんなサイズもフルスクリーンできることもですが、ディスプレイの解像度変更は再表示までに少し時間がかかる（2～5秒）のと、復帰時に他のウィンドウの位置やサイズが変わっていることがあるからです。環境によってはこれらが起こらないかもしれませんが…

あとこれも環境依存な気がしますが、どちらの方法でもHGIMG4版でフルスクリーン時にティアリングらしき現象が起こっています。
![ウィンドウモード撮影写真](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151962/b2b13853-4975-0c35-cb1c-301889c81e26.jpeg)
![フルスクリーンモード撮影写真](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151962/a32b4e7e-0912-b5d3-e13c-49da5382cf2a.jpeg)
上の2つの写真はスマホでそれぞれのモード時を撮影したものです（PCのスクリーンショット機能だと確認できない）が、フルスクリーンモードでラインが途中で切れているのが確認できます。シャッタースピードの関係上2本以上ラインが見えていますがプログラム上このラインは上から下まで繋がっているはずです。おそらく垂直同期が取れていないためだと思うのですが、なぜフルスクリーン時にだけこのようになるのかは分かりません。またディスプレイ解像度に依ってはフルスクリーンにしてもライン切れは起こりませんでした。ビデオデバイスの設定で垂直同期をオンにするとライン切れが解消されたので垂直同期が関係していると思うのですが…

# 更新情報

**2019.12.24** - ウィンドウ拡張版でウィンドウリサイズ・フルスクリーン時にmousex(y)の値がズレいていたのを修正、サンプルにマウス座標表示
**2020.02.08** - HSP3.6β2用にGistへのリンク追記、コメント修正・追記