はじめに
ゲームなどで使えるウィンドウモードとフルスクリーンモードを切り替える処理のサンプルです。マルチディスプレイ環境下でもそれぞれのディスプレイでフルスクリーンになります。
サンプルはフルスクリーン化の方法別に以下の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
ディスプレイ解像度変更版
スクリーンの大きさは変えずに、ディスプレイの解像度をスクリーンの大きさに合わせる方法です。スクリーンサイズ自体は変わらないのでもう一つの方法に比べて描画負荷は小さくなると思います。ただしディスプレイやビデオデバイスに依って対応解像度が決まっているので、どんなサイズでもフルスクリーンにできるわけではないことに注意が必要です。
; #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
ウィンドウ拡張版
ディスプレイの解像度は変えずに、スクリーンの大きさを解像度に合わせる方法です。もう一つの方法と比べどんなサイズでもフルスクリーンにできますが、バックバッファをメインウィンドウに拡縮コピーするという処理なので描画負荷は高めになると思います。こちらは処理上利用しやすいのでウィンドウモード時のリサイズ機能も付けてみました。
; #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版でフルスクリーン時にティアリングらしき現象が起こっています。
上の2つの写真はスマホでそれぞれのモード時を撮影したものです(PCのスクリーンショット機能だと確認できない)が、フルスクリーンモードでラインが途中で切れているのが確認できます。シャッタースピードの関係上2本以上ラインが見えていますがプログラム上このラインは上から下まで繋がっているはずです。おそらく垂直同期が取れていないためだと思うのですが、なぜフルスクリーン時にだけこのようになるのかは分かりません。またディスプレイ解像度に依ってはフルスクリーンにしてもライン切れは起こりませんでした。ビデオデバイスの設定で垂直同期をオンにするとライン切れが解消されたので垂直同期が関係していると思うのですが…
更新情報
2019.12.24 - ウィンドウ拡張版でウィンドウリサイズ・フルスクリーン時にmousex(y)の値がズレいていたのを修正、サンプルにマウス座標表示
2020.02.08 - HSP3.6β2用にGistへのリンク追記、コメント修正・追記