search
LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

ゲームなどで使えるウィンドウモードとフルスクリーンモードを切り替える処理のサンプルです。マルチディスプレイ環境下でもそれぞれのディスプレイでフルスクリーンになります。

サンプルはフルスクリーン化の方法別に以下の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版でフルスクリーン時にティアリングらしき現象が起こっています。
ウィンドウモード撮影写真
フルスクリーンモード撮影写真
上の2つの写真はスマホでそれぞれのモード時を撮影したものです(PCのスクリーンショット機能だと確認できない)が、フルスクリーンモードでラインが途中で切れているのが確認できます。シャッタースピードの関係上2本以上ラインが見えていますがプログラム上このラインは上から下まで繋がっているはずです。おそらく垂直同期が取れていないためだと思うのですが、なぜフルスクリーン時にだけこのようになるのかは分かりません。またディスプレイ解像度に依ってはフルスクリーンにしてもライン切れは起こりませんでした。ビデオデバイスの設定で垂直同期をオンにするとライン切れが解消されたので垂直同期が関係していると思うのですが…

更新情報

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

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
What you can do with signing up
2